Lets get this party started – Startup orchestration with GetIt

If you haven’t used GetIt before you might read this article first:
One to find them all.
If you are using GetIt for some time make sure to revisit the ReadMe in the API docs because a lot more was added in the last weeks. Like async factories and factories with parameters.

To use the features described in this post you need to use get_it V4.0.0 or higher. Please don’t be angry with me, it also brings minor breaking changes that improved the API.

All too often your app has to do a lot of initialization work before it really can get to its actual purpose. And often this includes several asynchronous function calls like:

  • Reading from shared_preferences
  • opening a database or file
  • Calling some REST API to get the latest data updates

To make things more complicated it can happen that one or more objects depend on others to be initialized before they can be initialized like

  1. Read API token from shared_preferences
  2. before you can make your first REST call
  3. before you can update your database.

You can orchestrate this sequence manually but its a tedious process. To make your life easier I included some functions in GetIt that do that job for you and integrates nicely in a Flutter project.

Why in GetIt and not a separate package you might ask? The reason is that the objects that you register in GetIt are most often the objects that need to get initialized. So it makes sense to combine these processes.

There are two ways you can orchestrate your start-up, one that is almost completely automatic and another one that allows you complete control over the moment when an object signals that its ready to be used.

So how does it work

GetIt offers a function allReady() that completes when all in GetIt registered objects have signalled that they are ready to use.

Future allReady(Timeout timeout)

The returned Future is ideally used as the source of a FutureBuilder like:

return FutureBuilder(
    future: getIt.allReady(),
    builder: (BuildContext context, AsyncSnapshot snapshot) {
      if (snapshot.hasData) {
          return Scaffold(
            body: Center(
                child: Text('The first real Page of your App'),
            ),
          );
      } else {
          return CircularProgressIndicator();
      }
    });

Your app can show some start-up page with an animation and switch the content as soon as allReady() completes.

If you want to switch-out the full page, instead of using a FutureBuilder you can do this in the initState() function of your StatefullWidget:

class _StartupPageState extends State<StartupPage> {
  @override
  void initState() {
    GetIt.I.allReady().then((_) => Navigator.pushReplacement(
        context, MaterialPageRoute(builder: (context) => MainPage())));
    super.initState();
  }

Orchestrating the start-up dance

Automatic

The easiest way to initialize a Singleton asynchronously is by using the new registerSingletonAsync function, which expects an async factory function. When that function has completed it notifies GetIt that this object is ready. As an example let’s take this class here:

class RestService {
  Future<RestService> init() async {
  // do your async initialisation...
  // simulating it with a Delay here
    await Future.delayed(Duration(seconds: 2));
    return this;
  }
}

All you have to do to make it signal its ready state is using registerSingletonAsync:

final getIt = GetIt.instance;

getIt.registerSingletonAsync<RestService>(() async {
    final RestService = RestService();
    await RestService.init();
    return RestService;
});

If your init function returns its instance like the RestService in the example above you can even write this shorter:

getIt.registerSingletonAsync<RestService>(() async => RestService().init());

As soon as the last Singleton has finished its factory function the Future from allReady() will complete and trigger your UI to change.

Manual

You might encounter cases where you need to separate the initialization from the factory function of a Singleton. Or maybe you want to start the initialisation as a fire end forget function from the constructor.
To still be able to synchronize it with other Singletons you can manually signal that your object is ready by using the signalReady() function. This requires to inform GetIt that this object will signal ready later so that GetIt knows it has to wait for that before completing the allReady() Future.
To do this you have two possibilities depending on your preferences:

  • Pass the optional signalsReady parameter to the registration functions
  • Make the type that you register implement the empty abstract class WillSignalReady. This has the advantage that the one who is registering the Singleton does not need to know how it will signal its ready state.

Here an example for the separation of creation and registration:

class ConfigService implements WillSignalReady {
  Future init() async {
    // do your async initialisation...
    GetIt.instance.signalReady(this);
  }
}


/// registering
getIt.registerSingleton<ConfigService>(ConfigService());

/// initializing as a fire and forget async call
getIt<ConfigService>().init();

As you can see we used the non async registration function in that case because we don’t need to. If your factory function needs to be async too you can also use registerSingletonAsync again.

A nice way to hide the whole initialisation is to start it as a fire and forget call from the constructor:

class ConfigService {
  ConfigService() {
    _init();
  }
  Future _init() async {
    // do your async initialisation...
    GetIt.instance.signalReady(this);
  }
}

Dealing with dependencies

Automatic synchronisation

If the singletons that require an async initialisation depend on each other we can use the optional parameter dependsOn of registerSingletonAsync and registerSingletonWithDependencies. The latter one is used if you have a Singleton that doesn’t need any async initialisation but that’s constructor depends on other singletons being ready.

Imaging this set of services:

dependencies

In code this could look like this:

getIt.registerSingletonAsync<ConfigService>(() async {
  final configService = ConfigService();
  await configService.init();
  return configService;
});

getIt.registerSingletonAsync<RestService>(() async => RestService().init());

/// this example uses an async factory function
getIt.registerSingletonAsync<DbService>(createDbServiceAsync,
    dependsOn: [ConfigService]);

getIt.registerSingletonWithDependencies<AppModel>(
    () => AppModelImplmentation(),
    dependsOn: [ConfigService, DbService, RestService]);

This will ensure that dependent singletons will wait with their construction until the ones they depend on have signalled their ready state

Be careful not to create circular dependencies. If you have some deadlocks between the different initialisation functions allReady or isReady with throw a WaitingTimeOutException which contains detailed information on who is waiting for whom at that moment.

Manual synchronisation

If somehow the automatic synchronisation does not fit your need, you can manually wait for another Singleton to signal ready by using the isReadyFunction:

/// Returns a Future that completes if the instance of an Singleton, defined by Type [T] or
/// by name [instanceName] or by passing the an existing [instance],  is ready.
/// If you pass a [timeout], an [WaitingTimeOutException] will be thrown if the instance
/// is not ready in the given time. The Exception contains details on which Singletons are
/// not ready at that time.
/// [callee] optional parameter which makes debugging easier. Pass `this` in here.
Future<void> isReady<T>({
  Object instance,
  String instanceName,
  Duration timeout,
  Object callee,
});

I hope you like the latest addition to GetIt and that it will make your app start-up easier then ever.

Contact me:

5 thoughts on “Lets get this party started – Startup orchestration with GetIt

  1. Bill says:

    Great work! Thanks! Can’t wait to update my project after vacation.

  2. […] Startup orchestration of your app. More on this you can find in my other post Lets get this party started […]

  3. marcin wasilewski says:

    getIt.registerSingletonAsync(() async => RestService().init());

    Did you forget to await? Shouldn’t be?

    getIt.registerSingletonAsync(() async =>await RestService().init());

    • Thomas Burkhart says:

      Hi,
      no this is correct.
      As the future is returned directly from the lambda function an await isn’t necessary.

Leave a Reply to One to find them all: How to use Service Locators with Flutter | Guru Meditation Cancel reply

Your email address will not be published. Required fields are marked *