Let's 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 one or more objects may 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 it's 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 it's ready to be used.

So how does it work

GetIt offers a function allReady() that completes when all in GetIt registered objects have signaled 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 {
  @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 been completed it notifies GetIt that this object is ready. As an example let\'s take this class here:

class RestService {
  Future 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 to use 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(() 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 initialization as a fire end forget function from the constructor. To still synchronize it with other Singletons you can manually signal that your object is ready by using the signalReady() function. This requires informing 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 to implement the empty abstract class WillSignalReady. This has the advantage that the one registering the Singleton does not need to know how it will signal its ready state.

Here is an example of the separation of creation and registration:

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

/// registering
getIt.registerSingleton(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 use registerSingletonAsync again.

A nice way to hide the whole initialization 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 initialization 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 initialization 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(() async {
  final configService = ConfigService();
  await configService.init();
  return configService;
});

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

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

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

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

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

Manual synchronization

If somehow the automatic synchronization 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 a Singleton, defined by Type [T] or
/// by name [instanceName] or by passing an existing [instance], is ready.
/// If you pass a [timeout], a [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 isReady({
  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 than ever.