Flutter GetX Get.back() or navigator.pop() deletes controller from memory and can not recreate it
Asked Answered
C

5

16

I have two pages: HomePage and DetailsPage and associated GetxControllers.

HomePage:

class HomePage extends GetView<HomeController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HomePage')),
      body: Container(
        child: Obx(
          () => ListView.builder(
            itemCount: controller.task.length,
            itemBuilder: (context, index) {
              return ListTile(
                leading: Text('${index + 1}'),
                title: Text(controller.task[index]["name"]),
                onTap: () {
                  Get.to(
                    DetailsPage(),
                    arguments: controller.task[index]["name"],
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }
}

HomeController:

class HomeController extends GetxController {
  final TaskRepository repository;
  HomeController({@required this.repository}) : assert(repository != null);

  final _task = [].obs;
  set task(value) => this._task.assignAll(value);
  get task => this._task;

  onInit() {
    super.onInit();
    getAllTask();
  }

  getAllTask() {
    repository.getAll().then((value) => task = value);
  }
}

As you can see the HomeController depends on a TaskRepository which is a mock repo.

And my DetailsPage:

class DetailsPage extends GetView<DetailsController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          GestureDetector(
            onTap: () {
              Get.back();
            },
            child: Row(
              children: [
                Icon(Icons.arrow_back),
                Text('Go Back'),
              ],
            ),
          ),
          Expanded(
            child: Center(
              child: Obx(
                () => Text(controller.taskDetail.value),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

DetailsController:

class DetailsController extends GetxController {
  final taskDetail = ''.obs;

  @override
  void onInit() {
    super.onInit();
    taskDetail.value = Get.arguments;
  }
}

I have created an AppDependencies class to initialize the dependencies (controllers, repositories, API clients, etc.):

class AppDependencies {
  static Future<void> init() async {
    Get.lazyPut(() => HomeController(repository: Get.find()));
    Get.lazyPut(() => DetailsController());
    Get.lazyPut(() => TaskRepository(apiClient: Get.find()));
    Get.lazyPut(() => TaskClient());
  }
}

I am initializing all the dependencies by calling AppDependencies.init() on main():

void main() async {
  await AppDependencies.init();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

HomePage

DetailsPage first time

Going back to HomePage and then Going again to DetailsPage

As you can see on the third image, going back from DetailsPage to HomePage and going back to DetailsPage causes an exception saying:

"DetailsController" not found. You need to call "Get.put(DetailsController())" or "Get.lazyPut(()=>DetailsController())"

But I already did that on main(). I also tried with Get.put() instead of Get.lazyPut() but I found that for Get.put() any dependencies of any other dependency must be registered before the dependent one. For example, HomeController depends on TaskRepository so TaskRepository must be before HomeController if using Get.put() like:

Get.put(TaskRepository());

Get.put(HomeController());

And this is not what I want because I don't wanna track what comes before what manually. And I found that this causes if there's a back button (which almost every page has).

What I am doing wrong here?

Cyanate answered 10/2, 2021 at 14:12 Comment(0)
S
9

If you don't want to use fenix = true, you can use something like this for example in your click method:

try {
   ///find the controller and 
   ///crush here if it's not initialized
   final authController = Get.find<AuthController>();

   if(authController.initialized)
     Get.toNamed('/auth');
   else {
     Get.lazyPut(() => AuthController());
     Get.toNamed('/auth');
   }

} catch(e) {

   Get.lazyPut(() => AuthController());
   Get.toNamed('/auth');
}

About memory, important to consider of fenix param:

The internal register of [builder()] will remain in memory to recreate the Instance if the Instance has been removed with [Get.delete()]. Therefore, future calls to [Get.find()] will return the same Instance.

Seanseana answered 9/6, 2021 at 15:21 Comment(0)
M
7

You need to bind all controller and the add in GetMaterialApp.

You facing this issue because of when you use back at that time it remove or delete controller like : [GETX] "LoginController" onDelete() called

For prevent this issue you need to create InitialBinding.

InitialBinding

class InitialBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => LoginController(LoginRepo()), fenix: true);
    Get.lazyPut(() => HomeController(HomeRepo()), fenix: true);
  }
}

In Main method :

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get.put(AppController());
    return GetMaterialApp(
      title: StringConst.APP_NAME,
      debugShowCheckedModeBanner: false,
      defaultTransition: Transition.rightToLeft,
      initialBinding: InitialBinding(),
      theme: ThemeData(
        primarySwatch: ColorConst.COLOR,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: RoutersConst.initialRoute,
      getPages: routes(),
    );
  }
}

Thanks

Milligram answered 30/3, 2021 at 2:45 Comment(4)
Will it affect the performance of the app.(if i had like 4 to 6 controllers)Bath
fenix: true will prohibit calling lifecycle methods like onInit() and onReady() which isn't a very expected behavior in the case of controllersCyanate
The named parameter 'fenix' isn't defined. what is fenix?Proem
Remember : fenix will work with Get.lazyPut() not with Get.put()Proem
I
3

try

Navigator.pop(context);

in place of

 Get.back();
Incompressible answered 13/9, 2022 at 6:42 Comment(0)
K
2

Updated answer with Bindings:

You can achieve greater control of how how and when you controllers initialize with bindings and smart management. So if you need the onInit to fire every time you go the page you can do so with bindings. Setup a dedicated bindings class for your details page.

class DetailsPageBinding extends Bindings {
  @override
  void dependencies() {
    // any controllers you need for this page you can lazy init here without setting fenix to true
  }
}

If you're not already using GetMaterialApp instead of MaterialApp you'll need to do so. I suggest throwing static const id = 'details_page'; on your page(s) so you don't have to mess with raw strings for routing.

A basic example of your GetMaterialApp would look like this.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: HomePage.id,
      title: 'Material App',
      getPages: [
        GetPage(name: HomePage.id, page: () => HomePage()),

// adding the new bindings class in the binding field below will link those controllers to the page and fire the dependancies override when you route to the page

        GetPage(name: DetailsPage.id, page: () => DetailsPage(), binding: DetailsPageBinding()),
      ],
    );
  }
}

Then you'll need to do your routing via

Get.toNamed(DetailsPage.id)

Original Answer:

Add fenix: true to your lazy init; Check the docs on lazyPut.

Get.lazyPut(() => HomeController(repository: Get.find()), fenix: true);
Kempe answered 10/2, 2021 at 15:54 Comment(5)
Thanks. I tried that too. But then it (controller) behaves like a singleton and can not call onInit() when navigating back to the page.Cyanate
Thanks! Just to know, what about the dependencies that aren't page-specific (in this case the repository and client) which may be used throughout different pages. Do I need to lazyPut them in every page binding? Or there's a better way to get rid of this repetition? For example the above approach I take except for page-specific controllers? Another thing (might be off-topic) what about UseCases in clean architecture? Should I register them globally like singleton with fenix: true? Or register them on pages binding?Cyanate
No you don't need to do it for every page. You can leave your AppDependancies setup in place for global stuff (or use the initialBinding field of GetMaterialApp). But the bindings for that page should fix the issue you're having with controller getting deleted and not recreated. With my example it will get deleted when you leave the page then reinitialized when you go back to it. As for your other question I'd say that's a case by case basis depending on your needs. There are scenarios where you might want to keep a controller in memory but that's beyond the scope of this discussion.Kempe
That being said, generally speaking, if you have controllers that are only needed for one page, then to me it makes to use bindings although by no means a requirement. Good explnation from GetX creator about bindings hereKempe
I am running into the same problem, except I'm using nested navigation as shown in this link - flutter.dev/docs/cookbook/effects/nested-nav . So I cannot use the getx navigator. Is there still a workaround for bindings?Lougheed
E
1

As always, I'm late, but I hope this can help others. You could have used somthing like this:

Get.put(Dependency(), permanent: true);

Using 'permanent: true' tells GetX not to remove the dependency under any circumstances, unless it is you who want to delete it from memory. I even use this for depdendency injection, injecting different dependencies according to the situation, like tests and production:

Get.put<Interface>(Dependency(), permanent: true);

Using the interface allows you to have multiple implementations of that interface according to your needs, and you can retrieve those seamlessly using

Get.find<Interface>();

Hope this helped.

Exacerbate answered 5/10, 2022 at 13:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.