flutterdartflutter-get

Are obs stream being closed automatically by GetxControllers?


I am using the following package https://pub.dev/packages/get. Do I need to close my .obs in the onClose of a GetxController? I can't find anything about this in the docs. And looking at my memory it appears that the are being destroyed automatically.


Solution

  • In my understanding of GetX + Flutter so far...

    No, you shouldn't have to remove .obs in the close() method of GetxControllers. Disposal of observables from a Controller are done automatically when the Controller is removed from memory.

    GetX disposes/removes GetxControllers (and their observables) when the widget in which they are contained are popped off the widget stack / removed from the widget tree (by default, but can be overridden).

    You can see this in the override of dispose() methods of various Get widgets.

    Here's a snippet of dispose() that's run when GetX widgets are popped/removed:

      @override
      void dispose() {
        if (widget.dispose != null) widget.dispose(this);
        if (isCreator || widget.assignId) {
          if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
            GetInstance().delete<T>(tag: widget.tag);
          }
        }
        subs.cancel();
        _observer.close();
        controller = null;
        isCreator = null;
        super.dispose();
      }
    

    When you use Bindings or Get.to() you're using GetPageRoute's which do cleanup by Route names:

      @override
      void dispose() {
        if (Get.smartManagement != SmartManagement.onlyBuilder) {
          WidgetsBinding.instance.addPostFrameCallback((_) => GetInstance()
              .removeDependencyByRoute("${settings?.name ?? routeName}"));
        }
        super.dispose();
      }
    

    Test App

    Below is a test App you can copy/paste into Android Studio / VSCode and run to watch the debug or run window output for GETX lifecycle events.

    GetX will log the creation & disposal of Controllers in and out of memory.

    GetX Output Log

    The app has a HomePage and 3 ChildPages using Get Controllers in 3 ways, all which remove itself from memory:

    1. GetX / GetBuilder
    2. Get.put
    3. Bindings
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    void main() {
      // MyCounterBinding().dependencies(); // usually where Bindings happen
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return GetMaterialApp(
          title: 'GetX Dispose Ex',
          home: HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('GetX Dispose Test'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                RaisedButton(
                  child: Text('GetX/Builder Child'),
                  onPressed: () => Get.to(ChildPage()),
                ),
                RaisedButton(
                  child: Text('Get.put Child'),
                  onPressed: () => Get.to(ChildPutPage()),
                ),
                RaisedButton(
                  child: Text('Binding Child'),
                  onPressed: () => Get.to(ChildBindPage()),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    /// GETX / GETBUILDER
    /// Creates Controller within the Get widgets
    class ChildPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('GetX Dispose Test Counter'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('This is the Child Page'),
                GetX<ChildX>(
                  init: ChildX(),
                  builder: (cx) => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
                ),
                GetBuilder<ChildX>(
                  init: ChildX(),
                  builder: (cx) => RaisedButton(
                    child: Text('Increment'),
                    onPressed: cx.inc,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    /// GET.PUT
    /// Creates Controller instance upon Build, usable anywhere within the widget build context
    class ChildPutPage extends StatelessWidget {
      //final ChildX cx = Get.put(ChildX()); // wrong place to put  
      // see https://github.com/jonataslaw/getx/issues/818#issuecomment-733652172
    
      @override
      Widget build(BuildContext context) {
        final ChildX cx = Get.put(ChildX());
        return Scaffold(
          appBar: AppBar(
            title: Text('GetX Dispose Test Counter'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('This is the Child Page'),
                Obx(
                  () => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
                ),
                RaisedButton(
                  child: Text('Increment'),
                  onPressed: cx.inc,
                )
              ],
            ),
          ),
        );
      }
    }
    
    class MyCounterBinding extends Bindings {
      @override
      void dependencies() {
        Get.lazyPut(() => ChildX(), fenix: true);
      }
    }
    
    /// GET BINDINGS
    /// Normally the MyCounterBinding().dependencies() call is done in main(),
    /// making it available throughout the entire app.
    /// A lazyPut Controller /w [fenix:true] will be created/removed/recreated as needed or
    /// as specified by SmartManagement settings.
    /// But to keep the Bindings from polluting the other examples, it's done within this
    /// widget's build context (you wouldn't normally do this.)
    class ChildBindPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        MyCounterBinding().dependencies(); // just for illustration/example
    
        return Scaffold(
          appBar: AppBar(
            title: Text('GetX Dispose Test Counter'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('This is the Child Page'),
                Obx(
                      () => Text('Counter: ${ChildX.i.counter}', style: TextStyle(fontSize: 20),),
                ),
                RaisedButton(
                  child: Text('Increment'),
                  onPressed: ChildX.i.inc,
                )
              ],
            ),
          ),
        );
      }
    }
    
    
    class ChildX extends GetxController {
      static ChildX get i => Get.find();
      RxInt counter = 0.obs;
    
      void inc() => counter.value++;
    }
    

    Notes

    Get.to vs. Navigator.push

    When using Get.put() in a child widget be sure you're using Get.to() to navigate to that child rather than Flutter's built-in Navigator.push.

    GetX wraps the destination widget in a GetPageRoute when using Get.to. This Route class will dispose of Controllers in this route when navigating away / popping the widget off the stack. If you use Navigator.push, GetX isn't involved and you won't get this automatic cleanup.

    Navigator.push

    onPressed: () => Navigator.push(context, MaterialPageRoute(
                      builder: (context) => ChildPutPage())),
    

    Get.to

    onPressed: () => Get.to(ChildPutPage()),