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


ATTENTION: This is an article about get_it which has nothing to do with the getX package please stop mixing them up.
Most people, when starting with Flutter, look for a way to access their data from views to keep them separate. The Flutter documentation recommends using an InheritedWidget. This not only allows data access from anywhere in the widget tree but also automatically updates widgets that reference it.
Some problems with InheritedWidgets
The biggest problem for me when I wrote this article was that I couldn't get only the parts of the widget tree to rebuild where the data had changed. Additionally, the need to have access to the BuildContext
limits its use to the UI part of your code.
I now know that you can make it work, but I still wonder why struggle with InheritedWidgets when there are easier solutions. The requirement for the BuildContext still remains.
Alternatives
Since the automatic updating of widgets that reference it isn't working perfectly, you might question why you should use an InheritedWidget
at all. Especially if you're using the InheritedWidget
just to access your model from anywhere, like when using BLOC or other reactive patterns, there are other solutions that might be even better.
Singletons
Singletons might be the first thing that comes to mind. While a Singleton makes it easy to access an object from anywhere, it complicates unit testing because you can only create one instance, making it hard to mock.
IoC containers with Dependency Injection
While this is a possible alternative for accessing an object and keeping it flexible for testing, I have some concerns about automatically injecting objects at runtime.
For me, it makes it harder to track where a specific object instance comes from, but that might just be personal preference.
Using an IoC container creates a network of dependent objects, which means that when you access the first object, all dependent objects are instantiated at the same time. This can impact startup performance, especially for mobile apps. Even objects that might be needed later could be created unnecessarily. (I know some IoCs offer lazy creation, but this doesn't fully solve the problem.)
IoC containers usually require some form of reflection to determine which objects need to be injected where. Since Dart doesn't support this in Flutter, it can only be addressed using code generation tools.
Provider / Riverpod
Provider is a strong alternative to get_it. However, I still think get_it is a good choice for these reasons:
Provider requires a
BuildContext
to access 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 not GUI elements but are needed to access the objects registered in Provider. Personally, I prefer to have as few non-UI Widgets in my widget trees as possible.
In general, the concept behind get_it is easier for many people to understand.
Although Riverpod is very powerful, its API is too complex, in my opinion.
With watch_it, get_it gets the simplest state management solution for Fluttter
There are now ways to access objects from Provider without the BuildContext, but I still prefer get_it’s API and features.
Service Locators
Like with IoCs, you need to register the types you want to access later. The difference is that instead of having an IoC inject instances automatically, you call the service locator directly to get the object you need.
Some people criticize this pattern, calling it old-fashioned and hard to test. However, as we will see, the testing concern isn't really valid. In my view, delivering software is more important than getting caught up in theoretical debates about the best pattern. For me and many others, Service Locators are a straightforward and practical solution.
A great advantage of using a Service Locator or IoC is that you are not restricted to using it inside a widget tree; you can use it anywhere to access any type of registered object.
GetIt the Service Locator for Dart
Coming from C#, I was used to a simple Service Locator (SL) called Splat. This inspired me to create something similar in Dart, resulting in the development of GetIt.
GetIt is very fast because it uses a Map<Type>
internally, allowing access in O(1) time.
GetIt is a singleton, so you can access it from anywhere using its instance
property or its shortcut:
final getItInstance = GetIt.instance;
//shortcut
final getItInstance2 = GetIt.I;
Usage
It's pretty straightforward. Typically, at the start of your app, you register the types you want to access later from anywhere in your app. After that, you can access instances of the registered types by calling the Service Locator again.
The nice thing is you can register an interface or abstract class along with a concrete implementation. When accessing the instance, you always request the interface or abstract class type. This makes it easy to switch the implementation by simply changing the concrete type at registration time.
Globals strike back or the Return of the Globals
One major difference from C# is that Dart allows the use of global variables. Although GetIt is a singleton, I like to assign its instance to a global variable to make accessing GetIt easier.
I can almost hear some of you cringe at the mention of 'global variable,' especially if, like me, you were always told that globals are bad. Recently, I learned a much nicer term for them: 'Ambient variables.' This might sound like a euphemism, but it actually describes their purpose better. These are variables that hold object instances defining the environment in which the app operates.
if you use my
watch_it
package you get a gloabaldi
instance to get_it included
Getting practical
I refactored a simple example to use GetIt instead of an inherited Widget. To set up the Service Locator, I added a new file called di.dart
, which also contains the global (ambient) variable for the Service Locator. This makes it easier to reference when writing unit tests.
// ambient variable to access the service locator
final di = GetIt.instance;
void setup()
{
di.registerSingleton(AppModel());
// Alternatively you could write it GetIt.I.registerSingleton(AppModel());
}
Someone recently pointed out that there is a conceptual difference between DI (Dependency Injection) and a Service Locator, and that I should not have named the global get_it instance
di
. In my view, this is more of an implementation detail because both DI and SL aim to achieve "inversion of control." This means you can control how an object or function behaves from the outside by injecting or locating objects.Because of the well-known abbreviation DI for dependency injection, I continue to use
di
as the variable name. Feel free to usesl
,locator
, or whatever you prefer.
GetIt offers various methods to register types. The registerSingleton
method ensures you always receive the same instance of the registered object.
When using the InheritedWidget, the definition of a button looked like this:
MaterialButton(
child: Text("Update"),
onPressed: TheViewModel.of(context).update
),
Now with GetIt it changes to
MaterialButton(
child: Text("Update"),
onPressed: di.get<AppModel>().update
),
Actually, because GetIt is a callable class we can write
MaterialButton(
child: Text("Update"),
onPressed: di<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 to be no longer necessary according to an issue in the Dart compiler. However, I would still choose to consistently use one method..
Registration in Detail
Different ways of registration
Besides the above used registerSingleton
there are two more ways to register types in GetIt
Factory
di.registerFactory( () => AppModelImplementation() );
If you register your type like this, each call to di.get<AppModel>()
will create a new instance of AppModelImplementation
, as long as it's a descendant of AppModel
. To do this, you need to pass a factory function to registerFactory
.
Sometimes, it's useful to pass different values to factories when calling get()
. For this, there are versions of 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 = di<AppModel>(param1: 'abc',param2:3);
LazySingleton
Creating the instance during registration can take time at app start-up. You can delay the creation until the object is requested for the first time using:
di.registerLazySingleton(() => AppModelImplementation());
Only the first time you call get<AppModel>()
, the factory function you provided will be executed. After that, you will always receive the same instance.
Applications beyond just accessing models from views
When using a service locator with interfaces or abstract classes (I really wish Dart still had interfaces), you gain a lot of flexibility in configuring your app's behavior at runtime:
Easily switch between different implementations of services. For example, define your REST API service class as an abstract class "WebAPI" and register it in the service locator with different implementations, such as various API providers or a mock class.
if (emulation) { di.registerSingleton( WeatherAPIEmulation() ); } else { di.registerSingleton(WeatherAPIOpenWeatherMap() ); }
Register parts of your widget tree as builder factories in the SL, and choose different builders at runtime based on the screen size (phone/tablet).
If your business objects or services need to reference each other, register them in GetIt.
Testing with GetIt
Testing with GetIt is very easy because you can register a mock object instead of the real one and then run your tests.
GetIt offers a reset()
method that clears all registered types, allowing you to start fresh in each test.
If you prefer to inject your mocks for the test, this pattern is recommended for objects that use the service locator:
AppModel([WeatherAPI? weatherAPI]): _weatherAPI = weatherAPI ?? di();
There's more
GetIt offers many more features than I've mentioned here. If you like GetIt, I recommend reading the GetIt Readme, where you will find:
Asynchronous Factories and Singletons
Functions to unregister or reset registered factories/singletons
How to create more than one instance of GetIt if needed
Registering multiple objects of the same type by name
Startup orchestration of your app. You can find more on this in my other post Let's get this party started
GetIt scopes, which make creating and disposing of objects based on your app's state very easy
And much more...
WatchIt
GetIt makes accessing your objects from anywhere very easy. To make it the simplest state management solution for Flutter, you can use its companion package, watch_it.
At https://github.com/escamoteur/flutter_weather_demo/tree/using_watch_it, you can find a version of the demo app that uses watch_it instead of a StreamBuilder. After registering the AppModel
as before, we can make the widget rebuild automatically every time the stream emits a new item.
class WeatherListView extends WatchingWidget {
WeatherListView();
@override
Widget build(BuildContext context) {
final snapshot = watchStream(
(AppModel model) => model.WeatherStream,
);
if (snapshot.hasData && snapshot.data!.length > 0) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) =>
buildRow(context, index, snapshot.data!));
} else {
return Text("No items");
}
}
watchStream
accesses the AppModel
inside GetIt and watches the WeatherStream
property.
You can make widgets rebuild with watch_it by watching:
Listenables / ValueListenables
Futures
Streams
Additionally, you can do almost anything that would typically require a StatefulWidget inside a StatelessWidget, which helps reduce a lot of boilerplate code.
Subscribe to my newsletter
Read articles from Thomas Burkhart directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
