jonataslaw / getx

Open screens/snackbars/dialogs/bottomSheets without context, manage states and inject dependencies easily with Get.
MIT License
10.34k stars 1.62k forks source link

It should be possible to use independent instances of controllers while using GetWidget. [Navigation, tags] #974

Open yasinarik opened 3 years ago

yasinarik commented 3 years ago

Versions

Flutter: 1.25.0-4.0.pre Get: 3.24.0

Running on macOS: 10.15.5 (19F101)

Problem

I want to use a new pair of View-Controller for each new route (named or normal route doesn't matter) while the main View widget of the route (and all of its children) extends on GetWidget for the caching advantage.

Please read carefully and provide a code sample because I couldn't find a way. The problem is pretty basic I think. If I am missing something, the solution will help me so much but if the GetX package currently lacks such a feature, it will be beneficial to everyone.

PS: I know this is not the place of asking questions but there is no one skilled enough that spent time on this critical problem in Telegram and Discord.

Reproducible Code & Steps

  1. Run the below code.
  2. It will show a screen with 3 buttons. Pink: Navigate back., Purple: Navigate to a new CounterScreen, Blue: Add a CounterItem to the horizontal ListView
  3. The purple button shows the current controller's hashcode in order to understand which instance of controller is used currently.
  4. Add a few CounterItems by tapping on the blue button.
  5. It will create Yellow CounterItemand add them into the horizontal ListView. Each of the yellow CounterItem shows the:
  1. Increase the individual counters by tapping a few times on any yellow button you've just added. We are doing this to change the state and making things more obvious. It is not necessary though.
  2. Now, by tapping on the Purplebutton, navigate to a new route which is a new instance of CounterView (the main GetWidget)
  3. As you can see, it navigates to a new route but the controller instance is exactly the same, so the yellow CounterItems too.
  4. The problem is, I've tried many different things over a week and asked this on community groups in Telegram and Discord; however, I couldn't manage to make a new instance of the same type of controller class to control this new route.
  5. Expected behavior is that I should have a new instance of the controller to control the new route. So, in the new route when you navigate, there should be no yellow CounterItems. It is important to note that I specifically want to make this by also using GetWidget.
  6. In the below code, there are some lines commented out just to show you that I've tried different combinations including Get.create(), Get.put()

// -getwidget-counterapp

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() async {
  runApp(App());
}

/* -------------------------------------------------------------------------- */
/*                                     App                                    */
/* -------------------------------------------------------------------------- */

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get.create(() => Controller());
    // Get.put(Controller());

    return GetMaterialApp(
      getPages: [
        GetPage(
          name: '/',
          page: () => CounterScreen(),
          binding: CounterBinding(),
        ),
        GetPage(
          name: '/CounterScreen',
          page: () => CounterScreen(),
          binding: CounterBinding(),
        ),
      ],
    );
  }
}

/* -------------------------------------------------------------------------- */
/*                                 Controller                                 */
/* -------------------------------------------------------------------------- */

class Controller extends GetxController {
  RxList<RxInt> counterList = <RxInt>[].obs;

  addCounterButton() {
    counterList.add(0.obs);
  }

  increaseSpecificCounter(int index) {
    counterList[index].value++;
  }
}

/* -------------------------------------------------------------------------- */
/*                               CounterBinding                               */
/* -------------------------------------------------------------------------- */

class CounterBinding implements Bindings {
  @override
  void dependencies() {
    // Get.create(() => Controller());
    Get.put(Controller());
  }
}

/* -------------------------------------------------------------------------- */
/*                                CounterScreen                               */
/* -------------------------------------------------------------------------- */

class CounterScreen extends GetWidget<Controller> {
  @override
  Widget build(BuildContext context) {
    // Get.create(() => Controller());
    // Get.put(Controller());

    return Scaffold(
      body: Obx(() {
        return Column(
          children: [
            Expanded(
              flex: 1,
              child: GestureDetector(
                onTap: () {
                  Get.back();
                },
                child: Container(
                  color: Colors.pink,
                  margin: EdgeInsets.all(16),
                  padding: EdgeInsets.all(16),
                  child: Text("Back to the previous route"),
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: GestureDetector(
                onTap: () {
                  // Get.to(CounterScreen(), preventDuplicates: false);
                  Get.toNamed("CounterScreen", preventDuplicates: false);
                },
                child: Container(
                  color: Colors.purple,
                  margin: EdgeInsets.all(16),
                  padding: EdgeInsets.all(16),
                  child: Text("TAP --> Navigate to a new CounterScreen \n " + controller.hashCode.toString()),
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: GestureDetector(
                onTap: () {
                  controller.addCounterButton();
                },
                child: Container(
                  color: Colors.blue,
                  margin: EdgeInsets.all(16),
                  padding: EdgeInsets.all(16),
                  child: Text("Add a counter button"),
                ),
              ),
            ),
            Expanded(
              flex: 3,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: controller.counterList.length,
                itemBuilder: (_, i) {
                  return CounterItem(i);
                },
              ),
            ),
          ],
        );
      }),
    );
  }
}

/* -------------------------------------------------------------------------- */
/*                                 CounterItem                                */
/* -------------------------------------------------------------------------- */

class CounterItem extends GetWidget<Controller> {
// class CounterItem extends StatelessWidget {
//   Controller controller = Get.find();

  final int index;

  CounterItem(
    this.index,
  );

  @override
  Widget build(BuildContext context) {
    return Obx(() {
      return GestureDetector(
        onTap: () {
          controller.increaseSpecificCounter(index);
        },
        child: Container(
          width: 260,
          height: 200,
          constraints: BoxConstraints(maxHeight: 200),
          color: Colors.yellow,
          margin: EdgeInsets.all(16),
          padding: EdgeInsets.all(8),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Text("#$index"),
              Text("Counter Value: " + controller.counterList[index].value.toString()),
              Text("Controller Hashcode: " + controller.hashCode.toString()),
              Text("How many buttons: " + controller.counterList.length.toString()),
            ],
          ),
        ),
      );
    });
  }
}
yasinarik commented 3 years ago

939

@jonataslaw here is another issue I have opened 12 days ago, which was well documented but I couldn't get an answer. In that particular issue, I have asked about using the "tags" property and automatically passing it to the children.

Since GetX has no inherited widget features, I couldn't manage it but instead, I've just used the Provider package which let me divide the widget tree into sections. (ForEx: a route is a section) So, the unique tag property can be found without manually passing it to the children. It is a huge time saver and the provider is only used for passing the "tag". The rest of the state & dependency management is done by GetX.

So, issue #939 is kinda related to this issue but they are not the same. Now, I want to use GetWidget because I have the data in lists and ListViews & InteractiveView. Since the GetWidget caches, it has a very good performance improvement.

However, GetWidget can't find the controller if I use a tag when injecting the dependency like Get.put(Controller(), tag: "aUniqueTag");

Let's think about this:

When I navigate to a new screen (it must be the same type as the current one), let it put another instance of the same controller and let the GetWidget find that particular controller instance, not the older one (but do not dispose of the first one because it should still control the first route).

Or let the GetWidget differentiate controllers by their "tag" property as Get.find<Controller>(tag: "uniqueTag") does.

jonataslaw commented 3 years ago

I'll take a look at it and give my opinion tomorrow

yasinarik commented 3 years ago

I'll take a look at it and give my opinion tomorrow

Well, ok then. Take your time @jonataslaw :)

yasinarik commented 3 years ago

Will you have any time for answering this? @jonataslaw

This is my second issue that I couldn't get an answer.

bajajsahil commented 2 years ago

@yasinarik Did you able to find a solution for that?

synstin commented 1 year ago

@yasinarik @jonataslaw any update?

similar https://github.com/jonataslaw/getx/discussions/2878 https://github.com/jonataslaw/getx/issues/939

RolandNaijuka commented 1 year ago

I had a similar issue. I solved it by using Get.find() to find the controllers for these pages. For instance in this sample example:

class Controller extends GetxController {}

I am instantiating it using Get.create

GetPage(
      name: '/simple-view',
      page: () => SimpleView(),
      binding: BindingsBuilder(() {
        Get.create(() => Controller());
      }),
    ),

in SimpleView, I am not using GetWidget to find the controller because it seems like GetWidget always removes all instances of Controller onClose. When I try to go back in the navigation route.

class SimpleView extends StatelessWidget {
  final controller = Get.find<Controller>(); // this creates a singleton for this specific page.

 SimpleView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
      final params = Get.parameters; // if you have parameters that you are passing to the page

      return Scaffold(
         body: Center(
             child: TextButton(onPressed: () => Get.toNamed("/simple-view", parameters: {'id': 'item2'}), child: Text('Navigate to item 2'))
         )
      );
   }
}
yasinarik commented 1 year ago

This is a pretty old issue I opened years ago.

My suggestion is that only use the state management part of GetX, if you want to do.

Always .put() controllers inside initState() of a stateful widget.

Delete a controller and kill the state by dispose() method of stateful widgets.

It is your responsibility to manage when to put or delete controllers.

Depend on Flutter default widget lifecycle. It will never disappoint you.

If you follow this structure, you can create apps in any complexity and scale without any issues.

inyong1 commented 1 year ago

How about GetBuilder It is similar to BlocProvider It will create new instance of the controller each time. GetBuilder( init: new MyController(), builder: .... )

yasinarik commented 1 year ago

How about GetBuilder

It is similar to BlocProvider

It will create new instance of the controller each time.

``

GetBuilder(

init: new MyController(),

builder: ....

)``

No. I highly suggest you not to use that.

Just handle put find delete of controller classes inside the initState and dispose methods of a stateful widget.

You can use tags by the way to differentiate between different instances of the same controller class.

jonataslaw commented 1 year ago

In fact, using a GetBuilder with the "global:false" setting has exactly the same behavior as creating a StatefulWidget and adding the instance to initState and removing it to dispose. In fact there is no reason to do this, if you don't want to use automatic dependency management, you can create a widget yourself that receives a controller, saves it in the state, and disposes of it in the dispose, this would follow the principles of DRY and avoid duplication of code.