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

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 BLOC 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.

Provider

Provider is a powerful alternative to GetIt. The reasons why I still think GetIt is a good choice are:

  • Provider needs a BuildContext to access the registered objects, so you can't use it inside business objects outside the Widget tree or in a pure dart package.
  • Provider adds its own Widget classes to the widget tree that are no GUI elements but are needed to access the in Provider registered objects. I personally want as view as possible non UI Widgets in my widget trees.

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).

GetIt itself is a singelton so you can access it from everywhere using its instance property or its shortcut:

final getItInstance = GetIt.instance;
//shortcut
final getItInstance2 = GetIt.I;

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.

Globals strike back or the Return of the Globals

One big difference to C# is that Dart allows the use of global variables. Although GetIt is a singelton I prefer to assign its instance to a global variable to minimize the code when I access GetIt.

I almost can hear 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.

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
final sl = GetIt.instance;

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

// Alternatively you could write it
  GetIt.I.registerSingleton<AppModel>(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:

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

Now with GetIt it changes to

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

Actually because GetIt is a callable class we can write

MaterialButton(
  child: 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.

This warning seems no longer be necessary according to an issue in the Dart compiler. I still would decide to use one way consequently.

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>( () => 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

In some cases its handy if you could pass changing values to factories when calling get(). For that there are variants for registering factories where the factory function takes two parameters:

void registerFactoryParam<AppModel,String,int>((title, size) 
 => AppModelImplementation(title,size));

When requesting an instance you pass the values for those parameters:

final instance = getIt<AppModel>(param1: 'abc',param2:3);

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>(() => AppModelImplementation())

Only the first time you call get<AppModel>() the passed factory function will be called. After that you will always receive the same instance.

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:
if (emulation)
{
   sl.registerSingleton<WeatherAPI>(WeatherAPIEmulation() );
}
else
{
   sl.registerSingleton<WeatherAPI>(new WeatherAPIOpenWeatherMap() );
}
  • Register parts of your widget tree as builders in the SL and register different builders at runtime depending on the screen size (phone/tablet)

  • If you business objects need to reference each other, register them in GetIt.

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>(); 

There's more

GetIt offers a lot more functions than I have described here. If you like GetIt I recommend reading the GetIt Readme there you will find:

  • Asynchronous Factories and Singeltons
  • Functions to unregister / reset registered factories/singletons
  • How to create more than one instance of GetIt if you really have the need.
  • Registering objects by name (although this should be your last resort)
  • Startup orchestration of your app. More on this you can find in my other post Lets get this party started
Contact me:

21 thoughts on “One to find them all: How to use Service Locators with Flutter

  1. Haram Bae says:

    Interesting article, Can I use this concept instead of BlocProvider?

    • Jonathan White says:

      Yes you could,

    • Mbote C. says:

      Actually, ever since I found this, I gave up on bloc providers etc. Thanks for this package. It’s a life saver.

      • Thomas Burkhart says:

        Thanks a lot

        • Chr. Marpert says:

          Same here, thanks a lot for the great package!

          I use get_it to inject the bloc instance into the bloc property of the BlocBuilder/BlocListener Widgets as I personally do not like passing things around the widget tree.

          Regarding flutter_bloc in combination with get_it I have a question:
          It seems when using the (Multi-)BlocProvider Widget, the bloc stream is closed automatically behind the scence, as far as I know.
          When using get_it, I think I have to manage the closing of the bloc my self, right?

          And if I may ask a second question regarding architectural patterns:
          Currently, I use to add a dependency (i.e. a repo or usecase class) to a comsuming class, (lets say a BloC), via the constructor in order to get loose coupling and simplify testing.

          Would you still pass these instances through the constructor, or rathre use it directly in the class wit sl.get() when needed?

          Thanks again!

  2. […] лично я предпочитаю Service Locator. На этот счет у меня есть специальная статья про GetIt — мою реализацию этого подхода, а здесь я […]

  3. […] лично я предпочитаю Service Locator. На этот счет у меня есть специальная статья про GetIt — мою реализацию этого подхода, а здесь я […]

  4. sinta says:

    Thank you very much for this article and package. Btw, I think you mean BLOC instead of BLOCK, and I almost can hear instead of I almost can here. (I am not a native English speaker, so I might be wrong.)

  5. Danny says:

    “When accessing an `Inherited’…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.”

    This is not true. As you pass a context down the tree it replaces any other versions with the lowest ancestor. Any lookup of an inhertied widget is an O(1) operation as it is held in a hashmap. See here for documentation: https://api.flutter.dev/flutter/widgets/BuildContext/inheritFromWidgetOfExactType.html

    • Thomas Burkhart says:

      I’m not sure about that. Because you don’t have the same context in every Build Method. For my understanding every Statefull Widget creates a new Context that is passed down to the children. If so it would not be O(1) but O(n). I try to get a clear answer to this from Google.

  6. Mohamed Zayani says:

    Very nice and simple. Thanks for the article.

  7. […] If you haven’t used GetIt before you might read this article first: One to find them all […]

  8. wandy says:

    is overriding registration only affect at the time I override or it changed entirely even when I call it again

  9. Praveen Soni says:

    Hi Thomas,
    Why you recommend import project files as packages ?
    Can you please help me understand why you recommending this ?

    • Thomas Burkhart says:

      Hi, meanwhile this isn’t necessary anymore. In the past you got problems if you mixed package style and local imports.
      this was fixed in the Dart compiler

  10. Abdulmumin says:

    This is a tool that has made my work easier. I really enjoyed your article. I also liked other features the package offers e.g scopes… I think this is one of the best every body needs.

  11. Rob says:

    Thanks for this post, really helpful since Provider gave me headaches 😉

Leave a Reply

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