Getx: InitialBinding is cleared when using Get.off

Created on 16 Jun 2020  ยท  8Comments  ยท  Source: jonataslaw/getx

I create a simple Login flow in Get for demo purpose.
https://gist.github.com/stefandevo/65f818d9cf748d4b9eb7aed1d439fb0c

This demo1 works but Splash > SignUp are still there when the user do a login and comes to the HomePage.

When I use Get.of in second demo:
https://gist.github.com/stefandevo/3e14e38c449da182d0b79e4551414b49

flutter: [GET] GetMaterialController has been initialized
flutter: [GOING TO ROUTE] /
flutter: [GET] ProfileController has been initialized
flutter: [GET] SignupController has been initialized
flutter: [REPLACE ROUTE] /
flutter: [NEW ROUTE] /signuppage
flutter: [GET] SharedPreferences deleted from memory
flutter: [GET] onClose of ProfileController called
flutter: [GET] ProfileController deleted from memory
flutter: [GET] onClose of SignupController called
flutter: [GET] SignupController deleted from memory

โ•โ•โ•โ•โ•โ•โ•โ• Exception caught by widgets library โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
The following message was thrown building SignupPage(dirty):
 SignupController not found. You need call put<SignupController>(SignupController()) before

It seems that the InitialBinding registrations are removed once the Get.off(SignupPage(), binding: SignupBinding());.

Furthermore, also the SignupController is deleted from memory?

I would think that InitialBinding is always keeping everything in the state manager.

Furthermore, can you comment on the used technique for this flow which is in ALL projects dealing with a logged in user. (startup flow).

Most helpful comment

void main() async {
print(1);
Future.delayed(Duration.zero, ()=> print(2));
print(3);
print(4);
print(5);
print(6);
}

out:

3
4
5
6
2

Bindings are synchronous, and are called when a route is pushed, and before it is built.

The problem is that when we insert an asynchronous operation in a Binding, it will only be ready after the page's build method is called, because Dart only resolves an asynchronous operation after ALL synchronous operations are called. So both the animation of the routes, as the initState, as the onInit of the Controller, and the build method of the entire screen, will enter the "synchronous" queue and will break the time that should be allocated to the asynchronous operation.
If you enter 1 million synchronous events right after declaring an asynchronous operation. All synchronous events will be performed first.

I would like to do something like:
await bindings.dependencies;
buildTransitions.build();

But a lot of inconsistent things happen when I try to insert an asynchronous operation between the call event of calling a route and displaying it.

initialBinding in GetMaterialApp is called in GetMaterialApp's "initState", so any asynchronous operations it has on it will be triggered only after the entire MaterialApp is built.

So in version 2 I only have the option of preloading asynchronous services in the main method to be loaded before MaterialApp.
In version 3 there will be a "preload", where the MaterialApp will only be called after these services are loaded, and initialBinding will be called by default in this preload.

All 8 comments

Almost always using initialBinding will cause problems with smartManagements other than keepFactory.
I would do something like this:

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(DemoApp());
}

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Demo',
      initialBinding: InitialBinding(),
      smartManagement: SmartManagement.keepFactory,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Splash(),
    );
  }
}

class InitialBinding extends Bindings {
  @override
  void dependencies() async {
    final prefs = await SharedPreferences.getInstance();
    Get.put<SharedPreferences>(prefs, permanent: true);
    Get.put<ProfileController>(ProfileController(), permanent: true);
  }
}

class Splash extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("loading..."),
      ),
    );
  }
}

class ProfileController extends RxController {
  static const profileKey = 'profileId';

  var isLoggedIn = false.obs;
  var user = '';

  static ProfileController get to => Get.find();

  @override
  void onInit() {
    final prefs = Get.find<SharedPreferences>();
    user = prefs.getString(profileKey);
    isLoggedIn.value = (user != null);
    ever(isLoggedIn, checkLoggedInStatus);
    checkLoggedInStatus(null);
  }

  void checkLoggedInStatus(_) {
    if (isLoggedIn.value) {
      Get.offAll(HomePage());
    } else {
      Get.offAll(SignupPage(), binding: SignupBinding());
    }
  }

  void signup() {
    final prefs = Get.find<SharedPreferences>();
    user = 'John Doe';
    prefs.setString(profileKey, user);
    isLoggedIn.value = true;
  }

  void logout() {
    final prefs = Get.find<SharedPreferences>();
    prefs.remove(profileKey);
    isLoggedIn.value = false;
  }
}

class SignupBinding extends Bindings {
  @override
  void dependencies() {
    Get.put<SignupController>(SignupController());
  }
}

class SignupController extends RxController {
  void signup() {
    final profileController = Get.find<ProfileController>();
    profileController.signup();
  }
}

class SignupPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.find<SignupController>();
    return Scaffold(
      appBar: AppBar(
        title: Text('Signup'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            RaisedButton(
              child: Text("Do Signup"),
              onPressed: () => controller.signup(),
            )
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            Container(
              child: Text('Welcome ' + ProfileController.to.user),
            ),
            RaisedButton(
              child: Text("Logout"),
              onPressed: () => ProfileController.to.logout(),
            )
          ],
        ),
      ),
    );
  }
}

What I change in the code?

1- SmartManagement.keepFactory
2- I made isLoggedIn observable with var isLoggedIn = false.obs;
3- Added ever(isLoggedIn, checkLoggedInStatus); and checkLoggedInStatus(null);
The first is just an ever that will observe the change in the variable. The second is a check that receives null because callbacks are mandatory with ever, and we are taking advantage of the same method to make the first check.

Why are we giving a first check, if we are changing the value above?
Well, the first thing is that there may be problems with the login. I have already done tests on a virtual device with little ram to test behavior of state managers when the system has little or no ram and the application will crash. With that I was able to reproduce some unusual situations:
When the user presses a button, onPressed is expected to run immediately, and this will happen, however if the device receives a freeze at that time due to the transition animations that consume some ram (and will cause an immediate bottleneck) and the user you can click again during the freeze, and every time he touches the button, an onPressed will be scheduled to be called, and after stabilization we will have many onPresseds running.
Now imagine isLoggedIn being bombarded with bools, and you are listening for changes to it, this could have unwanted effects. Get luckily has flow control, so it definitely won't make any difference to Get. However, this brings us to an unusual situation, what to do with Booleans that only have a value of "true / false"? and if its default status is "false" and I add "false" again, it will never trigger ever (), because you haven't changed the state of that variable. So in this case, I manually checked the initial state change.
Is there a more elegant way of doing this? Of course, using Rx instead of extensions.
You can create an observable variable in 3 ways, the first is by adding an .obs, the second is by creating an Rx(), and the third is simply using the "X" classes that Get provides.
StringX, ListX, BoolX, IntX, MapX, and etc.
I rarely use them, but in situations where I need to initialize a value other than the initial value to fire an ever, they are useful and even more elegant. In the example, we would just put ever on top of isLoggedIn and everything would happen normally, without any problem:

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(DemoApp());
}

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Demo',
      initialBinding: InitialBinding(),
      smartManagement: SmartManagement.keepFactory,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Splash(),
    );
  }
}

class InitialBinding extends Bindings {
  @override
  void dependencies() async {
    final prefs = await SharedPreferences.getInstance();
    Get.put<SharedPreferences>(prefs, permanent: true);
    Get.put<ProfileController>(ProfileController(), permanent: true);
  }
}

class Splash extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("loading..."),
      ),
    );
  }
}

class ProfileController extends RxController {
  static const profileKey = 'profileId';

  var isLoggedIn = BoolX(); // can be Rx<Bool>() too
  var user = '';

  static ProfileController get to => Get.find();

  @override
  void onInit() {
    final prefs = Get.find<SharedPreferences>();
    user = prefs.getString(profileKey);
    ever(isLoggedIn, checkLoggedInStatus);
    isLoggedIn.value = (user != null);
  }

  void checkLoggedInStatus(_) {
    if (isLoggedIn.value) {
      Get.offAll(HomePage());
    } else {
      Get.offAll(SignupPage(), binding: SignupBinding());
    }
  }

  void signup() {
    final prefs = Get.find<SharedPreferences>();
    user = 'John Doe';
    prefs.setString(profileKey, user);
    isLoggedIn.value = true;
  }

  void logout() {
    final prefs = Get.find<SharedPreferences>();
    prefs.remove(profileKey);
    isLoggedIn.value = false;
  }
}

class SignupBinding extends Bindings {
  @override
  void dependencies() {
    Get.put<SignupController>(SignupController());
  }
}

class SignupController extends RxController {
  void signup() {
    final profileController = Get.find<ProfileController>();
    profileController.signup();
  }
}

class SignupPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.find<SignupController>();
    return Scaffold(
      appBar: AppBar(
        title: Text('Signup'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            RaisedButton(
              child: Text("Do Signup"),
              onPressed: () => controller.signup(),
            )
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            Container(
              child: Text('Welcome ' + ProfileController.to.user),
            ),
            RaisedButton(
              child: Text("Logout"),
              onPressed: () => ProfileController.to.logout(),
            )
          ],
        ),
      ),
    );
  }
}

However, I plan to create a more elegant solution for this in the future.

Just knew that we can declared like this

var isLoggedIn = BoolX(); // can be Rx<Bool>() too

Thanks

@jonataslaw thanks for the answer and sample code.

  1. Is there a way without the use of the intermediate Splash screen in between? Being able to set the home page is one of these things that you use a lot based upon a setting.
  2. Get.putAsync does not support the permanent property

Furthermore, if I used the sample from the ReadMe

    Get.putAsync<SharedPreferences>(() async {
      final prefs = await SharedPreferences.getInstance();
      return prefs;
    });

in the InitialBinding instead of the code you provided, I get following error:

flutter: [GET] GetMaterialController has been initialized
flutter: [GOING TO ROUTE] /
[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception:  SharedPreferences not found. You need call put<SharedPreferences>(SharedPreferences()) before
#0      GetInstance.find 
package:get/src/get_instance.dart:154
#1      Storage.find 
package:get/src/extension_instance.dart:12
#2      ProfileController.onInit 
package:hashting_mobile/demo2.dart:62
#3      DisposableInterface.onStart 
package:get/โ€ฆ/rx/rx_interface.dart:43
#4      GetInstance.initController 
package:get/src/get_instance.dart:123
#5      GetInstance.find 
package:get/src/get_instance.dart:148
#6      GetInstance.put 
package:get/src/get_instance.dart:48

So is this an issue with putAsync ?

I wouldn't use putAsync for your preferences, it tends to have wierd side effects. I just made a PreferencesController and gave it a static async method called load that loads my preferences and returns an instance of a PreferencesController. Then I put it with permanent to true. Works like a charm, haven't had any problem with that approach thus far.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final PreferencesController preferencesController = await PreferencesController.load();
  Get.put<PreferencesController>(preferencesController, permanent: true);
  runApp(App());
}

Wow, when you said on your readme that not even half of Get is documented i thought you were exaggerating, but man, i never heard in my life of these other two forms of declaring an obs. Nice one!

after 3.0 i want to talk with you about things that not has any documentation so we can add them. I can't recognize them in the code, so you would need to tell me ๐Ÿ˜…

@jonataslaw thanks for the answer and sample code.

  1. Is there a way without the use of the intermediate Splash screen in between? Being able to set the home page is one of these things that you use a lot based upon a setting.
  2. Get.putAsync does not support the permanent property

Furthermore, if I used the sample from the ReadMe

    Get.putAsync<SharedPreferences>(() async {
      final prefs = await SharedPreferences.getInstance();
      return prefs;
    });

in the InitialBinding instead of the code you provided, I get following error:

flutter: [GET] GetMaterialController has been initialized
flutter: [GOING TO ROUTE] /
[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception:  SharedPreferences not found. You need call put<SharedPreferences>(SharedPreferences()) before
#0      GetInstance.find 
package:get/src/get_instance.dart:154
#1      Storage.find 
package:get/src/extension_instance.dart:12
#2      ProfileController.onInit 
package:hashting_mobile/demo2.dart:62
#3      DisposableInterface.onStart 
package:get/โ€ฆ/rx/rx_interface.dart:43
#4      GetInstance.initController 
package:get/src/get_instance.dart:123
#5      GetInstance.find 
package:get/src/get_instance.dart:148
#6      GetInstance.put 
package:get/src/get_instance.dart:48

So is this an issue with putAsync ?

@jonataslaw can you have a look at my questions here pls? Thanks :-)

void main() async {
print(1);
Future.delayed(Duration.zero, ()=> print(2));
print(3);
print(4);
print(5);
print(6);
}

out:

3
4
5
6
2

Bindings are synchronous, and are called when a route is pushed, and before it is built.

The problem is that when we insert an asynchronous operation in a Binding, it will only be ready after the page's build method is called, because Dart only resolves an asynchronous operation after ALL synchronous operations are called. So both the animation of the routes, as the initState, as the onInit of the Controller, and the build method of the entire screen, will enter the "synchronous" queue and will break the time that should be allocated to the asynchronous operation.
If you enter 1 million synchronous events right after declaring an asynchronous operation. All synchronous events will be performed first.

I would like to do something like:
await bindings.dependencies;
buildTransitions.build();

But a lot of inconsistent things happen when I try to insert an asynchronous operation between the call event of calling a route and displaying it.

initialBinding in GetMaterialApp is called in GetMaterialApp's "initState", so any asynchronous operations it has on it will be triggered only after the entire MaterialApp is built.

So in version 2 I only have the option of preloading asynchronous services in the main method to be loaded before MaterialApp.
In version 3 there will be a "preload", where the MaterialApp will only be called after these services are loaded, and initialBinding will be called by default in this preload.

OK clear! Thanks.
I will close this issue!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

williamsilva-98 picture williamsilva-98  ยท  4Comments

aytunch picture aytunch  ยท  4Comments

GoldenSoju picture GoldenSoju  ยท  3Comments

omartinma picture omartinma  ยท  3Comments

rupamking1 picture rupamking1  ยท  3Comments