One to find them all: How to use Service Locators with Flutter

Most people when starting with Flutter will start looking for a way how to access their data from the views to separate them. What’s recommended in the Flutter docs is using an InheritedWidget which not only allows to access data from anywhere in the Widget tree but also should be able to automatically update widgets that reference it.

Some problems with InheritedWidgets

Any descendant of InheritedWidget should be immutable which means you cannot change its data but have to create a new instance with new data. To be able to do this inside the widget tree you always have to wrap it in a StatefulWidget.

If an InheritedWidget changes not only the Widgets that reference it will be updated, because not the Widgets but the next surrounding context get registered. Which means that at least this full context including all its children will be rebuilt. Meaning if you place your inherited Widget at the very base of your tree it will rebuild the whole tree.

When reading the docs you get the impression that it shouldn’t be that way but neither I nor other developers I know have managed it that only referencing widgets get rebuilt.

So you would have to add different InheritedWidgets at different places inside your tree to minimize the rebuilt.

If someone can show me how it’s done the right way so that it works like expected I would me happy

When accessing an `Inherited by writing:

MyInheritedWidget.of(context).myProperty

Flutter will look into the next outer context and check if its registered in there. If not it will walk up the tree and check in the outer contexts too, meaning depending how deep you are in the tree this can take some time.

Alternatives

Given that the automatic updating of referencing widgets seems not to be optimal implemented we might ask why should I use an InheritedWidget at all.
Especially if you are using the InheritedWidget only to access your model from anywhere e.g. when using the BLOCK or other reactive patterns there are other solutions that might even be better.

Singletons

Might be the very first thing that springs to mind. While a Singleton greatly solves the job of making it easy to access an object from anywhere it makes unit testing very difficult because you only can create one instance of it and its hard to mock.

IoC containers with Dependency Injection

While this is a possible alternative to solve accessing a certain object while keeping it flexible to test I have some objection against automagical injection of objects at runtime.

  • At least for me it makes it more difficult to follow where a certain object instance is coming from. But that’s a matter of taste I guess
  • Using an IoC container creates a network of dependent object which has the result that when you access the first object inside this network all dependent objects will be instantiated at the same moment which can hurt performance at start-up time especially for mobile apps. Even objects that might only be needed at a later time might be created without need. (I know that there are IoCs that offer lazy creation but that doesn’t completely solve this problem)
  • IoC containers typically need some sort of reflection to figure out which objects have to be injected where. As Dart doesn’t support in Flutter this can only be solved using code generation tools.

Service Locators

Like with IoCs you have to register types that you want to access later. The difference is that instead of letting an IoC inject instances automatically you call the service locator explicit to give you the desired object.

I know there are many people that have objections against this pattern calling it old fashioned and hard to test although the later one isn’t really true as we will see. IMHO it’s far more important to get software out of the door instead spending a lot of time with theoretical discussions which is the best possible pattern. For me and many others Service Locators are just a straight forward practical pattern

One nice side effect of using an Service Locator or IoC is that you are not limited to use it inside a widget tree but you can use it anywhere to access any type of registered objects.

GetIt a Service Locator for Dart

Coming from C# I was used to use a very simple Service Locator (SL) called Splat. So I tried if I could write something similar in Dart too with the result of GetIt.

GetIt is super fast because it uses just a Map<Type> inside which makes accesses to it O(1).

Globals strike back or the Return of the Globals

One big difference to C# is that Dart allows the use of global variables. Which means I didn’t need to implement GetIt as singleton or static class to access it from anywhere in the App.

I almost can here some of you shudder when reading the word ‘global variable’ especially if you are an old timer like me who was always told globals are bad. Not long ago I learned a much nicer term for them: ‘Ambient variables’ which might sound a bit like a hyphenism but actually describes the intention much better. These are variables that keep objects instances that define the ambience in which this app runs.

Usage

It’s pretty straight forward. Typically at the start of your app you register the types that you want later access from anywhere in your app. After that you can access instances of the registered types by calling the SL again.

The nice thing is you can register an interface or abstract class together with a concrete implementation. When accessing the instance you always ask for the interface/abstract class type. This makes it easy to switch the implementation by just switching the concrete type at registration time.

Getting practical

I refactored a very simple example to use GetIt instead of an inherited Widget. To initialize the SL I added a new file service_locator.dart which also contains the global (ambient) variable for the SL. That makes it also easier to reference it when writing unit tests.

// ambient variable to access the service locator
GetIt sl = new GetIt();

 void setup() {
      sl.registerSingleton<AppModel>(new AppModel());
}

GetIt has different methods to register types. registerSingleton ensures that you always get the same instance of the registered object.

Using the InheritedWidget the definition of a button looked like:

new MaterialButton(
        child: new Text("Update"),
        onPressed: TheViewModel.of(context).update
        ),

Now with GetIt it changes to

new MaterialButton(
        child: new Text("Update"),
        onPressed: sl.get<AppModel>().update
        ),

Actually because GetIt is a callable class we can write

new MaterialButton(
        child: new Text("Update"),
        onPressed: sl<AppModel>().update
        ),

Which is pretty concise.

you can find the whole code for the SL version of this App here https://github.com/escamoteur/flutter_weather_demo/tree/using_service_loactor

Extremely important if you use GetIt: ALWAYS use the same style to import your project files either as relative paths OR as package which I recommend. DON’T mix them because currently Dart treats types imported in different ways as two different types although both reference the same file.

Registration in Detail

Different ways of registration

Besides the above used registerSingleton there are two more ways to register types in GetIt

Factory

sl.registerFactory<AppModel>( () => new AppModelImplementation()) )

If you register your type like this, each call to sl.get<AppModel>() will create a new instance of AppModelImplementation given that it’s an descendent of AppModel. For this you have to pass a factory function to registerFactory

LazySingleton

As creating the instance on registration can be time consuming at app start-up you can shift the creation to the time the object is the first time requested with:

sl.registerLazySingleton<AppModel>(() => new AppModelImplementation())

Only the first time you call get<AppModel>() the passed factory function will be called.

Applications beyond just accessing models from views

When using an SL together with interfaces/abstract classes (I really wished Dart would still have interfaces) you get extremely flexible in configuring your apps behaviour at runtime:

  • Easy switching between different implementations of services E.g. define your REST API service class as abstract class “WebAPI” and register it in the SL with different implementations like different API providers or a mock class:
// Important to register services that might be used in AppModel constructor first
sl.registerSingleton<WeatherAPI>(new WeatherAPIEmulation() );
//sl.registerSingleton<WeatherAPI>(new WeatherAPIOpenWeatherMap() );
sl.registerSingleton<AppModel>(new AppModel());
  • Register parts of your widget tree in the SL and register different implementations at runtime depending on the screen size (phone/tablet)

Overriding registrations

You are not limited to register any type at start-up. You can do it also later.
If necessary you even can override an existing registration.
By default you will get an assertion if you try to register a type that is already registered because most of the time this might not your intention. But if you need to do it you can by setting GetIt’s property allowReassignment=true.

Testing with GetIt

Testing with GetIt is very easy because you can easily register a mock object instead the real one and then run your tests.

Get it offers a reset() method that clears all registered types so that you can start with a clean slate in each test.

If you prefer to inject you mocks for the test this pattern for objects that use the SL is recommended:

AppModel([WeatherAPI weatherAPI = null]): 
  _weatherAPI  = weatherAPI ?? sl<WeatherAPI>(); 
Contact me:

Leave a Reply

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