jonataslaw / getx

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

Visual Studio Code hot reload delete a GetxController from memory #749

Open darwin-morocho opened 4 years ago

darwin-morocho commented 4 years ago

I'am using the GetX pattern using get_cli so for all pages I'm using bindings to inject my controllers. It works fine but when I change something in my code and save it vscode applies hot reload in that moment the controller of my current page is deleted from memory so I need reload the app to taste my new changes

Flutter Version: 1.22.2

Getx Version: 3.15.0

Describe on which device you found the bug: Pixel 3a API 30 - Android Emulator.

Minimal reproduce code

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: "APP_NAMEr",
      onInit: this._init,
      onDispose: this._disponse,
      initialBinding: SplashBinding(),
      home: SplashPage(),
      getPages: AppPages.routes,
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('es'),
      ],
    );
  }
}

My pages

class AppPages {
  static final routes = [
    GetPage(
      name: Routes.SPLASH,
      page: () => SplashPage(),
      binding: SplashBinding(),
    ),
    GetPage(
      name: Routes.OFFLINE,
      page: () => OfflinePage(),
      binding: OfflineBinding(),
    ),
    GetPage(
      name: Routes.HOME,
      page: () => HomePage(),
      binding: HomeBinding(),
    ),
  ];
}

Binding

import 'package:app/app/modules/home/home_controller.dart';
import 'package:get/instance_manager.dart';

class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<HomeController>(
      () => HomeController(),
    );
  }
}

my view

class HomePage extends StatelessWidget {
  final _innerDrawerKey = GlobalKey<InnerDrawerState>();
  @override
  Widget build(BuildContext context) {
    final Responsive fr = Responsive.of(context);
    return GetBuilder<HomeController>(
      autoRemove: false,
      builder: (_) => InnerDrawer(
        key: _innerDrawerKey,
        swipe: false,
        rightChild: DrawerContent(_innerDrawerKey),
        scaffold: Scaffold(
          backgroundColor: Colors.white,
          appBar: HomeAppBar(_innerDrawerKey, fr: fr),
          body: SafeArea(
            child: Container(
              width: double.infinity,
              height: double.infinity,
              color: Color(0xfff0f0f0),
              child: Stack(
                children: [
                  Column(
                    children: [
                      Expanded(
                        child: DinaTabView(
                          children: [
                            HomeTab(),
                            PoliciesTab(),
                            HistoryTab(),
                            ContactUsTab(),
                          ],
                          tabController: _.tabController,
                        ),
                      ),
                      HomeTabBar(),
                    ],
                  ),
                  GpsStatus(),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

home controller

class HomeController extends GetxController {
  DinaTabController _tabController = DinaTabController();
  DinaTabController get tabController => _tabController;
  RxInt _currentTab = 0.obs;
  int get currentTab => _currentTab.value;

  set currentTab(int value) {
    if (_currentTab.value != value) {
      _currentTab.value = value;
      _tabController.index = value;
    }
  }

  @override
  void onInit() async {
    super.onInit();
  }

  @override
  void onClose() {
  }
}

THANKS FOR THE ASWER

eduardoflorence commented 4 years ago

Hi @the-meedu-app, I was unable to reproduce the problem with your code, as there are many parts missing, so I made a basic code and the controller is not being removed from memory when I do a hot reload. Could you test with this code below to see if it happens on your computer? Perhaps it is some widget (for example, InnerDrawer) that is causing this. Feel free to change the code and try to cause the problem.

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

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/home',
    getPages: [
      GetPage(
        name: '/home',
        page: () => HomePage(),
      ),
      GetPage(
        name: '/login',
        page: () => LoginPage(),
        binding: LoginBinding(),
      ),
    ],
  ));
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HOME')),
      body: Center(
        child: RaisedButton(
          onPressed: () => Get.toNamed('/login'),
          child: Text('Login'),
        ),
      ),
    );
  }
}

class LoginBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<LoginController>(() => LoginController());
  }
}

class LoginController extends GetxController {
  final message = 'Welcome'.obs;
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Container(
        child: GetBuilder<LoginController>(
          autoRemove: false,
          builder: (_) => Text('Message: ${_.message.value}')
        ),
      ),
    );
  }
}
darwin-morocho commented 4 years ago

Hi @the-meedu-app, I was unable to reproduce the problem with your code, as there are many parts missing, so I made a basic code and the controller is not being removed from memory when I do a hot reload. Could you test with this code below to see if it happens on your computer? Perhaps it is some widget (for example, InnerDrawer) that is causing this. Feel free to change the code and try to cause the problem.

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

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/home',
    getPages: [
      GetPage(
        name: '/home',
        page: () => HomePage(),
      ),
      GetPage(
        name: '/login',
        page: () => LoginPage(),
        binding: LoginBinding(),
      ),
    ],
  ));
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HOME')),
      body: Center(
        child: RaisedButton(
          onPressed: () => Get.toNamed('/login'),
          child: Text('Login'),
        ),
      ),
    );
  }
}

class LoginBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<LoginController>(() => LoginController());
  }
}

class LoginController extends GetxController {
  final message = 'Welcome'.obs;
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Container(
        child: GetBuilder<LoginController>(
          autoRemove: false,
          builder: (_) => Text('Message: ${_.message.value}')
        ),
      ),
    );
  }
}

I'am using the package inner_drawer so I not sure if the problem is the InnerDrawer because it is a child of a GetBuilder. I will remove InnerDrawer to check if the problem persists

eduardoflorence commented 4 years ago

Ok, I await your feedback

pasblin commented 4 years ago

Hi, Same for me, controller removed on hot reload . I am using drawer, and I think it happens in the navigation (Get.back(); ) after doing a hot reload. Regards

darwin-morocho commented 4 years ago

Ok, I await your feedback

Same problem removing InnerDrawer widget

darwin-morocho commented 4 years ago

Ok, I await your feedback

Same problem removing InnerDrawer widget

the problem occurs event if the screen is just using a controller with a simple from. I'm not sure if the problem only happens when I'am using bindings to inject my controller in the page

prakash-indorkar commented 4 years ago

I am having the same issue .. I use hot restart to reload controllers.. as hot reload does not show any changes. Most likely it's deleting GetXController.. could it be because I'm using BottomNavigationBar and loading pages on body: on change of selected index?

eduardoflorence commented 4 years ago

@the-meedu-app, @pasblin or @prakash-indorkar, can you put more parts of your code so that we can reproduce the problem just by copying and pasting your code in my editor? You can omit the confidential parts by placing a container. I can debug with the GetX package source code

prakash-indorkar commented 4 years ago

Hi Sorry I shared my code through live share link which might not be helpful.

Sharing link for repo in GitHub.. code is in .zip file. This is in very early stage.. I still learning, and from beginning it self I am using GetX. Please have a look.

https://github.com/prakash-indorkar/sharing.git

Thanks once again.

Sent from Mailhttps://go.microsoft.com/fwlink/?LinkId=550986 for Windows 10

From: Eduardo Florencemailto:notifications@github.com Sent: Wednesday, 4 November 2020 5:11 AM To: jonataslaw/getxmailto:getx@noreply.github.com Cc: prakash-indorkarmailto:prakash.indorkar@outlook.com; Mentionmailto:mention@noreply.github.com Subject: Re: [jonataslaw/getx] Visual Studio Code hot reload delete a GetxController from memory (#749)

@the-meedu-apphttps://github.com/the-meedu-app, @pasblinhttps://github.com/pasblin or @prakash-indorkarhttps://github.com/prakash-indorkar, can you put more parts of your code so that we can reproduce the problem just by copying and pasting your code in my editor? You can omit the confidential parts by placing a container. I can debug with the GetX package source code

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/jonataslaw/getx/issues/749#issuecomment-721431754, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AONSOGW6X3TO6OOGJCW3GDTSOCIIZANCNFSM4TCNB6YQ.

eduardoflorence commented 4 years ago

@prakash-indorkar, what are the steps to reproduce the problem after running the app?

prakash-indorkar commented 4 years ago

As per your recommendation, I used lazyput with fenix.

class AuthBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => AuthService(), fenix: true); Get.lazyPut(() => AuthController(), fenix: true); Get.lazyPut(() => UserController(), fenix: true); Get.lazyPut(() => HomeController(), fenix: true); Get.lazyPut(() => SharingService(), fenix: true); //Get.lazyPut(() => SharingController(), fenix: true); <=========== If I put this my sharing page do not show anything. Get.put(SharingController(), permanent: true); <==== If I use this .. SharingHomepage works fine. }

Not exactly the steps but .. as soon as I login and in the homepage .. first time it loads .. the if I move to other pages .. and come back on SharingHomePage.. contents not visible ( due to deletion of SharingControllers lazy instance.. ) If I use Get.put with permanent true. then it works fine.. Also I want to findout how to update the page based on streams of other collection data. For example in my case.. SharingHomePage is the main page and all the data is coming is from the Sharing collection.. a part of data is saved in Users collection which I am listing on users stream. So if any changes happen on Sharing Stream the page refresh and show new data. But when data related to Sharing in Users stream changes .. it does refresh the page. Please forward any example using firebase and getx pattern. if any ..

Thanks for your help, really appreciated. Regards, prakash

prakash-indorkar commented 4 years ago

haha .. I figured it out..

I used different tags for each page binding using BindingsBuilder().

Here is my workaround .. It may be helpful if anyone using BottomNavigationBar with GetX.

Please have a look if anything can be improved in my code..

HomePage.dart class HomePage extends GetWidget { final int pageIndex; HomePage({this.pageIndex}) { if (this.pageIndex != null) { controller.currentIndex = this.pageIndex; } } @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( appBar: AppBar( title: Obx(() => Text(currentPageTitle.value)), ), drawer: AppDrawer(), body: GetX( builder: () => HomeController.to.pages[controller.currentIndex], ), bottomNavigationBar: GetX( builder: () { return BottomNavyBar( selectedIndex: controller.currentIndex, onItemSelected: (index) { controller.currentIndex = index; }, items: [ BottomNavyBarItem(icon: Icon(Icons.home), title: Text('Home'), activeColor: Colors.orange), BottomNavyBarItem(icon: Icon(Icons.favorite), title: Text('Watchlist'), activeColor: Colors.red), BottomNavyBarItem(icon: Icon(Icons.person), title: Text('My Listings'), activeColor: Colors.blue), BottomNavyBarItem(icon: Icon(Icons.local_florist), title: Text('Add New'), activeColor: Colors.green), BottomNavyBarItem(icon: Icon(Icons.message), title: Text('Inbox'), activeColor: Colors.purple), ], ); }, ), ), ); } }

* HomeController.dart *** class HomeController extends GetxController { static HomeController to = Get.find();

final _currentIndex = 0.obs;

final List pages = [ MainHomePage(), Watchlist(), MyListings(), AddNew(), Inbox(), ];

int get currentIndex => _currentIndex.value;

set currentIndex(value) { if (_currentIndex.value != value) { _currentIndex.value = value; } } }

//...page_routes.dart.....................

GetPage(
  name: '/home',
  page: () => HomePage(),
  binding: BindingsBuilder(() {
    switch (HomeController.to.currentIndex) {
      case 0:
        Get.lazyPut(() => SharingController(), tag: 'mainhomepage');
        break;
      case 1:
        Get.lazyPut(() => SharingController(), tag: 'mywatchlist');
        break;
      case 2:
        Get.lazyPut(() => SharingController(), tag: 'mylistings');
        break;
      case 3:
        Get.lazyPut(() => SharingController(), tag: 'addnew');
        break;
      case 4:
        Get.lazyPut(() => SharingController(), tag: 'inboxpage');
        break;
      default:
        Get.lazyPut(() => SharingController(), tag: 'mainhomepage');
    }
  }),
),

Please note I am using custome BottomNavigationBar => BottomNavyBar .. but the logic is same..

Thanks, Prakash

ZenMittal commented 3 years ago

I am also using bottomNavigation and Get can't find controller for my GetView extended page:

// Controller page
class ProfileController extends GetxController {
  final text= 'example text';
}

// View Page
class ProfileView extends GetView<ProfileController> {

  @override
  Widget build(BuildContext context) {
    return Container(child: Text(controller.text));
}

// route config
    GetPage(
      name: Routes.PROFILE,
      page: () => ProfileView(),
      binding: ProfileBinding(),
    ),

//route name
abstract class Routes {
  static const PROFILE = '/profile';
}

//binding
class ProfileBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<ProfileController>(
      () => ProfileController(),
    );
  }
}

This code returns this error: "ProfileController" not found. You need to call "Get.put(ProfileController())" or "Get.lazyPut(()=>ProfileController())"

I am using the same configuration in other places of my code as well. Those pages are not connected with BottomNavigation, and Get is able to find controller for them just fine. But the above profile page is navigated to using bottomNavigation.

prakash-indorkar commented 3 years ago

Somehow lazyPut() did not work for me. I had to use put() instead and its working fine.. don't know how costly it would be in memory size..

eduardoflorence commented 3 years ago

@ZenMittal see an example here: https://github.com/jonataslaw/getx/issues/799#issuecomment-730719165

ad-on-is commented 3 years ago

I have exactly the same problem, which occurs in the following (very simplified) scenario.

I have a Products widget that looks like this and is included in two separate page widgets

class Products extends StatelessWidget {
 final String someParam;
  Products({this.someParam = 'foo'});

  @override
  Widget build(BuildContext context) {
    return GetBuilder<ProductsController>(
        init: ProductsController(),
        builder: (controller) {
            return ProductList();
        });
  }
}

class ProductList extends GetView<ProductsController> {
 return Text(controller.anotherParam);
}

Then I have 2 Page widgets which look like this

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetBuilder(
      init: Page1Controller(),
      builder: (controller) {
        return Products(someParam: 'bar');
      },
    );
  }
}
class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetBuilder(
      init: Page2Controller(),
      builder: (controller) {
        return Products(someParam: 'foo');
      },
    );
  }
}

The problem occurs when switching pages

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetBuilder(
      init: AppController(),
      builder: (controller) {
        if(controller.page == 1) {
           return Page1();
        }  else {
           return Page2();
         }
      },
    );
  }
}

So what happens:

[GETX] "AppController" has been initialized
[GETX] "Page1Controller" has been initialized
[GETX] "ProductsController" has been initialized

hot-reload works fine!

now, switch to Page2

[GETX] "Page2Controller" has been initialized
[GETX] "ProductsController" onClose() called
[GETX] "ProductsController" deleted from memory
[GETX] "Page1Controller" onClose() called
[GETX] "Page1Controller" deleted from memory

hot-reload gives the following error:
"ProductsController" not found. You need to call "Get.put(ProductsController())"
Notice, how another [GETX] "ProductsController" has been initialized is missing here

switch back to Page1

[GETX] "Page1Controller" has been initialized
[GETX] "ProductsController" has been initialized
[GETX] "Page2Controller" onClose() called
[GETX] "Page2Controller" deleted from memory

hot-reload works fine

The ProductsController is not inited when switching to Page2, and therefore gives an error.

Workarounds found so far

When preventing ProductsController from removing with autorRemove: false, this (obviosly) does not trigger the onInit() of ProductsController anymore, when switching pages.

I could get the ProductsController re-initilized when I render Products after the PageXController adds a delay of 1 second to the rendering-process. PageXController.dart


onInit() {
    setBusy(true);
    update();
    Future.delayed(Duration(milliseconds: 1000), () {
      setBusy(false);
      update();
    });
    super.onInit();
}

Page.dart

return controller.isBusy
            ? Text('Test')
            : Products(someParam: 'foo');

Solution

After fiddling around I finally found the solution.

So, to use GetView propperly, you have to init the corresponding controller with a tag, and pass that tag to your Widget.

class Products extends StatelessWidget {
 final String someParam;
  Products({this.someParam = 'foo'});

  @override
  Widget build(BuildContext context) {
    return GetBuilder<ProductsController>(
        init: ProductsController(),
        tag: 'tag-$someParam'
        builder: (controller) {
            return ProductList('tag-$someParam');
        });
  }
}

class ProductList extends GetView<ProductsController> {
  final String ctag;
  ProductList (this.ctag);
 @override
  String get tag => ctag;
Widget build(BuildContext context) {
 return Text(controller.anotherParam);
}
}

If done right you should see outputs like these

[GETX] "ProductsControllertag-foo" has been initialized
[GETX] "ProductsControllertag-foo" onClose() called
[GETX] "ProductsControllertag-foo" deleted from memory

[GETX] "ProductsControllertag-bar" has been initialized
...
hasnainadam7 commented 3 months ago

After Splash Screen it deltes some Repos and controllers idk why

[GETX] Instance "ProductRepoController" has been created [GETX] Instance "PopularProductRepo" has been created [GETX] Instance "ApiClient" has been created [GETX] Instance "ApiClient" has been initialized [GETX] Instance "PopularProductRepo" has been initialized [GETX] Instance "RecommendedProductRepo" has been created [GETX] Instance "RecommendedProductRepo" has been initialized [GETX] Instance "ProductRepoController" has been initialized [GETX] Instance "GetMaterialController" has been created [GETX] Instance "GetMaterialController" has been initialized [GETX] GOING TO ROUTE / [GETX] Instance "CartRepoController" has been created [GETX] Instance "CartRepo" has been created [GETX] Instance "SharedPreferences" has been created [GETX] Instance "CartRepoController" has been initialized [GETX] Instance "UserRepoController" has been created [GETX] Instance "UserRepo" has been created [GETX] Instance "UserRepoController" has been initialized [GETX] Instance "LocationRepoController" has been created [GETX] Instance "LocationRepo" has been created [GETX] Instance "LocationRepoController" has been initialized [GETX] REPLACE ROUTE / [GETX] NEW ROUTE /foodPage?pageId=0 [GETX] "SharedPreferences" deleted from memory [GETX] "CartRepo" deleted from memory [GETX] "CartRepoController" onDelete() called [GETX] "CartRepoController" deleted from memory [GETX] "UserRepo" deleted from memory [GETX] "LocationRepo" deleted from memory