GetX Controller not disposing off automatically
Asked Answered
P

7

12

I have a minimlaist sample app running on Android with GetX as State Management lib only. There are two screens LandingPage and MainScreen. On going back from MainScreen to LandingPage screen, the controller is not autodisposing as expected. I am using Flutter's Navigation only without wrapping with GetMaterialApp.

My expectation is that the value exposed by the controller should be reset to its initial value when the Controller is instantiated. However, the Widget continues to show the last value from the controller.

I am using the latest version of Flutter and GetX as avail of now : 2.2.3 and 4.3.8 respectively

Your help is appreciated.

Code:

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

class LandingScreen extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
  return Container(
   color: Colors.blue[800],
   child: Center(
     child: ElevatedButton(
       onPressed: () => {
         Get.to(MainScreen())
       },
       child: const Text('Navigate to Second Screen'),
     ),
    ),
  );
 }
}

class MainScreen extends StatelessWidget {
 final MyController controller = Get.put(MyController());

 @override
 Widget build(BuildContext context) {
  return Scaffold(
  body: SafeArea(
    child: Container(
      color: Colors.blueAccent,
      child: Center(
        child: Column(
          children: [
            Obx(() => Text('Clicked ${controller.count}')),
            FloatingActionButton(
              onPressed: controller.increment,
              child: Text('+'),
            ),
            ElevatedButton(
              onPressed: ()=>{Navigator.of(context).pop()},
              child: Text('Go Back'),
            )
          ],
          ),
         ),
        ),
       ),
      );
     }
    }

  class MyController extends GetxController {

   var count = 0.obs;
   void increment() => count++;

  }
Pointsman answered 5/9, 2021 at 10:47 Comment(7)
Yeah the controller wouldn't dispose until you use GetX navigation.Rexferd
Ok. I will try using GetX navigation and update. Is it mentioned anywhere in the docs though?Pointsman
I tried the GetX Navigation as well wrapping with GetMaterialApp only to find the same result with no luck :/. Any suggestions?Pointsman
I faced similar issues but after using getx navigation disposing work fine. can share the code?Rexferd
Sure, I'll do thatPointsman
@7mada, Can you mention the getx package version you're using?Pointsman
Ok, so I learnt that I had to pass a callback returning that Widget instead the Widget itself. It works fine now with GetMaterialApp.Pointsman
S
16

You need to use GetX navigation first. However, at the time of writing this answer, there is a bug causing the controllers not to get disposed off automatically. So, it is recommended for now to use bindings or to manually dispose of them from a StatefulWidget till the bug is fixed.

@override
  void dispose() {
    Get.delete<Controller>();
    super.dispose();
  }

Update 22 Nov. 2021 You can still use the above solution, or for a more elegant approach you can use Bindings along with extending GetView<YourController> instead of Stateful/ Stateless Widgets. GetXController will provide most of the functions that you would need from a StatefulWidget. You also don't have to use GetX Navigation.

This approach also works when using nested navigation.

To use Bindings, there are multiple methods explained here.

Squabble answered 22/9, 2021 at 10:13 Comment(2)
It's a supper answer , when we go to another screen controller will close and when we back it will reloadPaugh
You do not necessarily need to do that. There is no advantage of getx if one needs to dispose things manually.Pointsman
S
15

Automatic Disposal

To have our GetxController disposed when the widget is removed, use Get.put inside the build() method, not as a field. This is the correct usage as per GetX's maintainers.

The Controller can then be disposed when MainScreen is popped.

Incorrect Usage

Controller won't get disposed:

class MainScreen extends StatelessWidget {
 final MyController controller = Get.put(MyController());

 @override
 Widget build(BuildContext context) {
  return Scaffold(

Instantiation & registration (Get.put(...)) should not be done as a field.

Otherwise, the registration of controller is attached to the widget above (i.e. LandingScreen in question's example), not MainScreen. And MyController will only get disposed when LandingScreen is disposed. (In the question's code LandingScreen is the "home" Widget and its disposal only happens upon app exit.)

Why?

If instantiating a controller as a field, GetX will record that controller's creation with the context currently available, the parent widget's context, not the instantiating widget's context. The instantiating widget context isn't available until its build() method. A widget class' instantiation and the running of its build() method, are two distinct things. The build() method is not a factory constructor of the widget itself, but a lifecycle method used by the Flutter framework after the widget's instantiation, to inflate the widget and insert/mount it as an Element in the element tree of rendered objects. (That's my current understanding.)

In the question's example, MainScreen widget is instantiated in the context of LandingScreen. MainScreen and its fields are in the context of LandingScreen. MainScreen's build() method is run after its instantiation. MainScreen's context is available for use when its build() method is run, but not during the Widget's instantiation.

For MyController to be deleted/disposed when MainScreen is disposed, we must instantiate MyController inside of MainScreen context, which is available during the call of its build(BuildContext context) method. That's the context with which we should associate our GetxController if we want it automatically disposed of by GetX when the current/instantiating widget is removed from the widget tree.

Correct Usage

class MainScreen extends StatelessWidget {

 @override
 Widget build(BuildContext context) {
  // ↓ instantiate/register here inside build ↓
  final MyController controller = Get.put(MyController());
  return Scaffold(

Now when MainScreen is popped off the route stack, MyController will be cleaned up / disposed by GetX (which I assume observes the route history to know when to do its cleanup).

Stepchild answered 26/9, 2021 at 17:49 Comment(7)
What if I am using a StatefulWidget for any reason? Putting it in the build might not be a good idea?Squabble
@Squabble Hmm..., at first glance, I can't think of a reason why it would be bad to put it into the build() method of a StatefulWidget. But, it may make more sense to register a controller in a StatefulWidget's State object. When the State object is disposed, so will the controller.Stepchild
I'm not observing this behaviour, even though I'm calling Get.put inside the build method. Note that instead of Navigator.pop(context) I'm calling Navigator.of(context).pushNamedAndRemoveUntil.Schonfield
Thank you so much. I was initialising it outside build method which was preventing it to dispose after screen pop.Crouch
This is incorrect! Putting controllers inside build method is not needed for disposal of the controller.Pointsman
It does not make sense at all. Whenever a build method is called, the widget loose its state.Sauna
@CíceroMoura The original question is asking for exactly that: losing state not keeping state. The problem in original question is: a GetxController, when instantiated (improperly) as a field, its state is not lost when the current (stateless) widget is disposed. Reason: created as a field, GetxController is attached to the parent context, not the context of the current build() method.Stepchild
N
6

You can late initialize your controller like bellow:

late final YourController controller;

and inside your initState function:

@override
void initState() {
    super.initState();

    Get.delete<YourController>();
    controller = Get.put(YourController());
}

this will fix your problem

Nucleate answered 16/12, 2021 at 9:13 Comment(2)
This worked! But I would like to know what it is working.Plafond
@JohnMelodyMe that only delete then put again the controllerOx
D
1

You need to use getPages with navigation getx way

 Get.to(() => const NotificationsScreen())

routes:

GetMaterialApp(
          getPages: NavigatorService.onGenerateRoute(),
Diabolism answered 14/5, 2023 at 22:13 Comment(0)
W
1

define binding class in diffrent files.not use common file for define binding

Waldheim answered 5/7, 2023 at 11:28 Comment(0)
P
0

If you add the routes on the getMaterialApp you don't need to initiate the controller inside the build:

Add the 'initialRoute' and 'getPages' properties and remove the home one:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.purple,
  ),
  initialRoute: '/landing',
  getPages: [
    GetPage(name: '/landing', page: () => LandingScreen()),
    GetPage(name: '/main', page: () => MainScreen()),
  ],
  );
 }
}

On LandingPage() change the Get.to() to Get.toNamed():

class LandingScreen extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
  return Container(
   color: Colors.blue[800],
   child: Center(
     child: ElevatedButton(
       onPressed: () => Get.toNamed('/main'),
       child: const Text('Navigate to Second Screen'),
     ),
    ),
  );
 }
}

And on MainScreen change the navigator pop to Get.back():

class MainScreen extends StatelessWidget {
 final MyController controller = Get.put(MyController());

 @override
 Widget build(BuildContext context) {
  return Scaffold(
  body: SafeArea(
    child: Container(
      color: Colors.blueAccent,
      child: Center(
        child: Column(
          children: [
            Obx(() => Text('Clicked ${controller.count}')),
            FloatingActionButton(
              onPressed: controller.increment,
              child: Text('+'),
            ),
            ElevatedButton(
              onPressed: ()=>Get.back(),
              child: Text('Go Back'),
            )
          ],
          ),
         ),
        ),
       ),
      );
     }
    }
Photomural answered 18/12, 2021 at 17:5 Comment(0)
S
0

this kinda worked for me when i defined the following method in the controller.dart file:

late Readcontroller controller;

void dispose(){ Get.delete(); controller = Get.put(ReadController()); }

and then in the main file for me which was reading.dart:

widget build(Build context){ final ReadController controller = Get.put(ReadController()); }

the above piece of code just works fine for me

give it a try .......

}

Setback answered 11/2, 2022 at 11:6 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Frenum

© 2022 - 2024 — McMap. All rights reserved.