Flutter Community

Articles and Stories from the Flutter Community

Follow publication

How the GetIt service locator package works in Dart

Suragch
Flutter Community
Published in
9 min readJul 12, 2023

Unveiling the magic

magic

The GetIt package provides a convenient way to access app services and state management classes from anywhere within your app. This functionality is known as a service locator, which is often compared to dependency injection, another common way to decouple dependencies in your app.

The idea of dependency injection is easy enough (just pass your dependencies in as constructor arguments), but in a Flutter project I never really understood where to create those service objects in the first place. I like GetIt because it’s easy to use. You define all of your services in one file and then get them from anywhere in your app with a single line of code.

I started writing about GetIt a long time ago. Probably the earliest article was Creating services to do the work in your Flutter app. I didn’t understand how GetIt worked under the hood, though. It was a magic black box. Even in Flutter state management for minimalists, where I purposefully avoided all third-party state management packages, I still used GetIt. I wondered, though, do I really need GetIt? Could I just write the code myself? How does it even work? I thought about that even more after writing Thoughts on using third-party packages (or not).

Then today, I read the following tweet on Twitter by @dabit3 . I’m not so familiar with the author, but I agree with the sentiment:

In this article I’ll reveal the magic of how GetIt works. Then you can choose for yourself whether or not you want to use it or write your own service locator. If you choose to keep using GetIt (as I probably will), at least it won’t be unknown magic anymore.

A quick review

Before moving on to how GetIt works under the hood, let’s review how to use it.

Say you have a web API class that you want to access from several places within your app:

// storage_service.dart

abstract interface class StorageService {
Future<void> saveData(String data);
}

class WebApi implements StorageService {
@override
Future<void> saveData(String data) async {
// make http request
}
}

You can use GetIt for that. First, add get_it to your dependencies in pubspec.yaml:

dependencies:
get_it: ^7.6.0

Then make a file to register any services you plan to use in your app:

// service_locator.dart

import 'package:get_it/get_it.dart';
import 'storage_service.dart';

final getIt = GetIt.instance;

void setupServiceLocator() {
getIt.registerLazySingleton<StorageService>(() => WebApi());
}

And call that method before your Flutter app starts:

// main.dart

import 'service_locator.dart';

void main() {
setupServiceLocator();
runApp(const MyApp());
}

Then you can use GetIt to obtain a reference to your WebApi instance from some state management class like so:

import 'storage_service.dart';
import 'service_locator.dart';

class MyStateManager {
void doSomething() {
final storage = getIt<StorageService>();
storage.saveData('Hello world');
}
}

That’s the review. Now you’ll learn how to build that functionality yourself.

Building GetIt yourself

The GetIt package is by Thomas Burkhart. The source code is in a Flutter Community GitHub repo. The current version of the code is somewhat complex, so it helped me understand how the code worked by going all the way back to version 1.0. I won’t follow the exact same naming conventions or coding style as the internal GetIt code, but it’ll be close.

I’ll write this article as a tutorial, so create a new Flutter project and follow along with me.

Making GetIt a singleton

Create a new file in the lib folder named get_it.dart. Then add the following code:

class GetIt {
GetIt._instance();
static final instance = GetIt._instance();
}

This makes GetIt a singleton class. The private named constructor GetIt._instance() ensures that users won’t be able to create a new instance of GetIt using the default constructor GetIt(). The static instance will always be the same value. (See this post for other ways of creating singletons in Dart.)

Now your class will enable you to write something like this (as you saw previously using the get_it package):

final getIt = GetIt.instance;

Three ways to register an object

There are three main ways to register a service with GetIt.

Note: Actually, nowadays there are more than three, but in this tutorial we’ll just recreate the original three.

They’re the following:

  1. Register a factory: This means that every time you request your service object, GetIt will create a new instance of it for you. Think of it like brand-new objects coming out of a factory. This is useful when you want to reinitialize the state, such as with a state management class for a Flutter UI screen that starts out fresh each time you navigate to it.
  2. Register a singleton: You create a new instance of your service class at the time you register it with GetIt. Then GetIt always gives you that same instance back. This is useful when you want to keep the state of some service that you reference in multiple parts of your app. An example might be a database helper object.
  3. Register a lazy singleton: Here you register your service class, but GetIt doesn’t actually create an instance of it until you request it for the first time. After that, it always returns the same instance. This is useful when you want your app to start up faster by delaying some of the initialization logic until you actually need it.

To prepare to implement these three ways of registration, add the following enum below your GetIt class:

enum _ServiceFactoryType {
factory,
singleton,
lazySingleton,
}

The official get_it package calls these alwaysNew, constant, and lazy, but I find the names above easier to remember since they’ll have a one-to-one naming match with the functions you’ll create later.

Holding the object or the object builder

The purpose of GetIt is to give you the object you ask for whenever you want it. As you saw in the previous section, though, sometimes GetIt creates a new instance and sometimes it gives you a reference to a previously created instance. That means GetIt needs to have one of two things:

  1. An instance of the object itself, or
  2. A function that will create the object.

Your next step will be to write a wrapper class that will encompass those two possibilities. Add the following class at the bottom of get_it.dart:

class _ServiceFactory<T> {
_ServiceFactory({
required this.type,
this.creationFunction,
this.instance,
});

final _ServiceFactoryType type;

T Function()? creationFunction;
T? instance;
}

Here are the notes:

  • _ServiceFactory holds either the instance of your service class or the creationFunction that GetIt will use to build the instance in the future.
  • The generic T is used to represent any service class type that you may wish to register. For example, WebApi or DataRepo or StorageService.
  • Both creationFunction and instance are nullable because when you register a service, you’re only going to specify one of them. The one you don’t specify will be null.
  • What’s not nullable, though, is the _ServiceFactoryType. That’s the value of the enum that you created in the last step. That is, factory, singleton, or lazySingleton.

Now that you have a way to hold the object or its creation function, you can proceed to the next step, the magic of GetIt itself.

Storing the registered objects

The magic way that GetIt uses to store all of your registered objects is… (drum roll)… a map!

That’s it. No mysterious data structure or complex storage algorithm. Just a plain old Dart Map.

Add the following line to your GetIt class:

final _map = <Type, _ServiceFactory>{};

Maps are collections of key-value pairs. The key is a Type and the value is a _ServiceFactory. You’ve already been introduced to _ServiceFactory, but Type might be new for you. The Type type — (That makes me want to type, “Are you the type to type the Type type on a typewriter?”) — anyway, joking aside, the Type type is used for holding the different kinds of types that you have in Dart. Here are some examples:

Type myType = int;
Type another = String;
Type example = WebApi;

What that means in your _map is that you can look up a type and then get a _ServiceFactory wrapper back. For example, you look up the WebApi class type and get back a _ServiceFactory that either has an instantiated WebApi object or a function that will create the object.

The advantage of using the map data structure is that it’s fast. Returning a value from a map is an O(1) constant time operation.

Creating the objects

Before you add the register methods to your GetIt class, you still need to do one more thing.

You need to add a method to your _ServiceFactory class that will handle when to create the object instances. Replace _ServiceFactory with the following complete implementation:

class _ServiceFactory<T> {
_ServiceFactory({
required this.type,
this.creationFunction,
this.instance,
});

final _ServiceFactoryType type;
T Function()? creationFunction;
T? instance;

T getObject() {
switch (type) {
case _ServiceFactoryType.factory:
return creationFunction!();
case _ServiceFactoryType.singleton:
return instance as T;
case _ServiceFactoryType.lazySingleton:
instance ??= creationFunction!();
return instance as T;
}
}
}

Note the following about getObject:

  • When the enum value is factory, you always return a new object of type T by calling creationFunction.
  • When the enum value is singleton, you just return the existing object.
  • When the enum value is lazySingleton, you create a new object if instance is null. Otherwise, you return the existing object.

Adding the API to register services

Next, add the following registerFactory method to your GetIt class:

void registerFactory<T>(T Function() create) {
final value = _ServiceFactory(
type: _ServiceFactoryType.factory,
creationFunction: create,
);
_map[T] = value;
}

This takes your creation function argument and wraps it in a _ServiceFactory class. Then you add this value to the map. The generic type T for the key will be the actual object type at run-time.

Add a similar method for registerSingleton:

void registerSingleton<T>(T instance) {
final value = _ServiceFactory(
type: _ServiceFactoryType.singleton,
instance: instance,
);
_map[T] = value;
}

And another one for registerLazySingleton:

void registerLazySingleton<T>(T Function() create) {
final value = _ServiceFactory(
type: _ServiceFactoryType.lazySingleton,
creationFunction: create,
);
_map[T] = value;
}

The only things that differed between these method bodies were the enum values and whether you provided the creationFunction or the instance value.

Making GetIt callable

As you saw in the quick review section at the beginning of this article, the get_it package allows you to get a registered object using the following syntax:

final myObject = getIt<MyServiceClass>();

Note the () parentheses at the end. getIt is an instance but you can call it like a function if you implement the call method. (See the callable objects documentation for more details about this topic.)

Add the following method to your GetIt class:

T call<T>() {
final serviceFactory = _map[T];
return serviceFactory!.getObject() as T;
}

This code looks up the class type in the service factory map and returns the instance of that type, either by creating a new instance or by returning the stored instance.

That’s it! Now you can delete get_it from pubspec.yaml. The code in the Quick Review section at the start of the article should still work the same.

Conclusion

The get_it package is much more sophisticated than what you built here. Nowadays, it handles async factories and scopes. It also has more error checking. However, if all you need are the basic features you built today, there’s no reason you can’t use your own implementation. I’m probably going to keep using the get_it package myself, but I like knowing how it works now. I hope you do, too.

Full code

// get_it.dart

class GetIt {
GetIt._instance();
static final instance = GetIt._instance();

final _map = <Type, _ServiceFactory>{};

T call<T>() {
final serviceFactory = _map[T];
return serviceFactory!.getObject() as T;
}

void registerFactory<T>(T Function() create) {
final value = _ServiceFactory(
type: _ServiceFactoryType.factory,
creationFunction: create,
);
_map[T] = value;
}

void registerSingleton<T>(T instance) {
final value = _ServiceFactory(
type: _ServiceFactoryType.singleton,
instance: instance,
);
_map[T] = value;
}

void registerLazySingleton<T>(T Function() create) {
final value = _ServiceFactory(
type: _ServiceFactoryType.lazySingleton,
creationFunction: create,
);
_map[T] = value;
}
}

enum _ServiceFactoryType {
factory,
singleton,
lazySingleton,
}

class _ServiceFactory<T> {
_ServiceFactory({
required this.type,
this.creationFunction,
this.instance,
});

final _ServiceFactoryType type;

T Function()? creationFunction;
T? instance;

T getObject() {
switch (type) {
case _ServiceFactoryType.factory:
return creationFunction!();
case _ServiceFactoryType.singleton:
return instance as T;
case _ServiceFactoryType.lazySingleton:
instance ??= creationFunction!();
return instance as T;
}
}
}
// service_locator.dart

import 'get_it.dart';
import 'my_state_manager.dart';
import 'storage_service.dart';

final getIt = GetIt.instance;

void setupServiceLocator() {
getIt.registerLazySingleton<StorageService>(() => WebApi());
// getIt.registerSingleton<StorageService>(LocalStorage());
getIt.registerFactory(() => MyStateManager());
}
// storage_service.dart

abstract interface class StorageService {
Future<void> saveData(String data);
}

class WebApi implements StorageService {
@override
Future<void> saveData(String data) async {
print('Saving to the cloud: $data');
}
}

class LocalStorage implements StorageService {
@override
Future<void> saveData(String data) async {
print('Saving to SQLite: $data');
}
}
// my_state_manager.dart

import 'service_locator.dart';
import 'storage_service.dart';

class MyStateManager {
void doSomething() {
final storage = getIt<StorageService>();
storage.saveData('Hello world');
}
}
// main.dart

import 'package:flutter/material.dart';
import 'my_state_manager.dart';
import 'service_locator.dart';

void main() {
setupServiceLocator();
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
child: const Text('Do something'),
onPressed: () {
final manager = getIt<MyStateManager>();
manager.doSomething();
},
),
),
),
);
}
}

Flutter Community
Flutter Community

Published in Flutter Community

Articles and Stories from the Flutter Community

Suragch
Suragch

Written by Suragch

Flutter and Dart developer. Twitter: @suragch1, Email: suragch@suragch.dev

Responses (4)

Write a response

clean and concise, keep writing. Thanks for producing great articles

Good insight into get it 😊

Great dive and engineering approach bottom-up. One think worth remembering while using get that it is best to use registerFactory as much as possible (for stateless objects). This way the memory footprint is much lower. Inagine many images on the…