How to implement Dependency Injection in Flutter

Sandro Maglione

Sandro Maglione

Mobile development

Creating a simple app in Flutter is fast and easy. When things start to scale though we need to be more pragmatic when designing the infrastructure of our app.

Dependency injection is a programming pattern which allows us to better organize and manage classes and dependencies inside our app.

In this post we are going to start from the basics of dependency injection and we will finish by looking at a complete setup using get_it and injectable.


What dependency injection is, and why I should use it

Dart at its core is an Object Oriented language. This brings all the advantages and disadvantages of the paradigm to your application.

One of the most common problems is the following: how to connect together different layers of my application?

Let us see a simple example to make this issue concrete.

Repository pattern in Flutter

The repository pattern is the most common strategy used to connect the UI layer and the business logic layer in a Flutter app.

You want to implement authentication in your app. There are countless solutions for this, but at its core all we generally need is a class with three functions:

  • signIn: verify the user identity and log in
  • signUp: create a new user account
  • signOut: allow the user to log out from the account

This is the perfect situation to use dart's abstract class (interface in other languages):

auth_repository.dart
abstract class AuthRepository {
  Future<String> signIn(String email, String password);
  Future<String> signUp(String email, String password);
  Future<void> signOut();
}
 
/// Do not focus on the return types, they are not important for this discussion 💁🏼‍♂️

What is the benefit here? Using an abstract class we can now be sure that every class that implements AuthRepository will allow our users to authenticate.

What implements does is forcing a class to define the methods declared inside the abstract class. This means that every class that implements AuthRepository is guaranteed to have at least signIn, signUp, and signOut as methods.

Who cares which services the app is using! We may have a SupabaseAuth, FirebaseAuth, CustomAuth. As long as all of these classes implements AuthRepository, we can swap different implementations as we need.

/// Do they `implements` `AuthRepository`? Yes, then all of the are accepted 👋
class SupabaseAuth implements AuthRepository { ... }
class FirebaseAuth implements AuthRepository { ... }
class CustomAuth implements AuthRepository { ... }

This pattern is generally called Repository pattern. This creates a layer of abstraction (abstract class) between the UI and the data layer.

In fact, the UI does not know where the data comes from. The UI only knows (and cares) about requesting and getting the data in the correct format.

How to access the repository in the UI

Now we come to the core of the problem: how can the UI code access the AuthRepository?

As always, there are many options to achieve the same result. Let's list what would be the ideal setup:

  • Possibility of swapping the concrete implementation (SupabaseAuth, FirebaseAuth, CustomAuth, TestingAuth) without accessing the UI code
  • Avoid initializing a new class every time when need to access the AuthRepository
  • Have a clear outlook on the dependencies between each layer of the application

This is where the Service Locator and Dependency Injection patterns come into play!

At the end of this discussion, we will be able to do something like the code below to sign in the user:

Future<void> _onClickSignIn() async {
  await getIt<AuthRepository>().signIn(email, password);
}

Service Locator pattern using get_it

get_it is the most used package to implement the Service Locator pattern in Flutter.

The idea is simple: we want a reliable way to register and access all objects inside our app. get_it allows us to do exactly that:

  • Register all objects (and their dependencies) at startup
  • Access all registered objects from a single source

How does this look like in our AuthRepository example?

First, we need to install get_it in our project by adding it to pubspec.yaml:

pubspec.yaml
name: flutter_supabase_complete
description: A new Flutter project.
publish_to: "none"
 
version: 1.0.0+1
 
environment:
  sdk: ">=2.17.6 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
 
  get_it: ^7.2.0

Then we need to register the concrete implementation of AuthRepository during the app startup. We can do that inside main:

main.dart
final getIt = GetIt.instance;
 
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  /// Use the global `getIt` instance to register `AuthRepository`
  getIt.registerSingleton<AuthRepository>(SupabaseAuth());
 
  runApp(App());
}

We defined the concrete implementation of AuthRepository as SupabaseAuth inside the global getIt object.

Now we can access AuthRepository everywhere in our UI code using getIt:

Future<void> _onClickSignIn() async {
  await getIt<AuthRepository>().signIn(email, password);
}

Using the repository pattern, the UI code now does not need to know how authentication is implemented: all it cares is that AuthRepository has a signIn method, that's it!

What is dependency injection then?

In our example, AuthRepository does not have any dependency. In reality we will have other layers responsible for other operations.

To expand on our example, let's introduce a Database Layer. This layer manages the interactions with an outside database.

We can use the same principle as before by defining an abstract class:

database.dart
abstract class Database {
  Future<String> verifyUserCredentials(String email, String password); 
}

We can then create different implementations of Database:

class SqlDatabase implements Database { ... }
class NoSqlDatabase implements Database { ... }
class LocalStorageDatabase implements Database { ... }

Database now becomes a dependency of a concrete implementation of AuthRepository:

supabase_auth.dart
class SupabaseAuth implements AuthRepository {
  final Database _database;
 
  const SupabaseAuth(this._database);
 
  ...
}

We have a new problem: we need to provide (inject) the Database dependency inside AuthRepository. Using get_it we have the following:

main.dart
final getIt = GetIt.instance;
 
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  /// Use the global `getIt` instance to register `AuthRepository`
  getIt.registerSingleton<Database>(SqlDatabase());
  getIt.registerSingleton<AuthRepository>(SupabaseAuth(getIt<Database>()));
 
  runApp(App());
}

This pattern is called Dependency Injection: we inject the Database dependency from outside SupabaseAuth.

This pattern gives us more control as opposed to initializing the dependency inside the class itself, which would look like this instead:

supabase_auth.dart
class SupabaseAuth implements AuthRepository {
  final Database _database;
 
  /// Quickly! Look at this and then forget 🏃‍♂️
  SupabaseAuth() {
    _database = SqlDatabase();
  }
 
  ...
}

How to manage dependencies at scale: injectable

This is all good and well, but it introduces another great hassle at some point.

Instead of me telling you about it, let me show you (example from real app back in the days):

main.dart
final serviceLocator = GetIt.instance;
 
Future<void> init() async {
  serviceLocator.registerFactory(
    () => QuizBloc(
      getQuizId: serviceLocator(),
      getQuizMeaningList: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => UserBloc(
      getSingleUser: serviceLocator(),
      getUserLocal: serviceLocator(),
      logoutUserLocal: serviceLocator(),
      experience: serviceLocator(),
      updateUserExperienceAndActivity: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => CollectionBloc(
      getCollectionList: serviceLocator(),
      userBloc: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => SearchBloc(
      getAllSearchMeaningList: serviceLocator(),
      percentageConverter: serviceLocator(),
      collectionBloc: serviceLocator(),
      userBloc: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => FullHistoryBloc(
      getFullHistoryList: serviceLocator(),
      dateTimeFormatter: serviceLocator(),
      searchBloc: serviceLocator(),
      userBloc: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => PacketBloc(
      getSavedPacketList: serviceLocator(),
      fullHistoryBloc: serviceLocator(),
    ),
  );
 
  serviceLocator.registerFactory(
    () => ExamBloc(
      chooseTranslationDirection: serviceLocator(),
    ),
  );
 
  serviceLocator.registerFactory(
    () => MemorytBloc(),
  );
 
  serviceLocator.registerFactory(
    () => PuzzleCellBloc(
      boardSize: BOARD_SIZE,
    ),
  );
 
  serviceLocator.registerFactory(
    () => PuzzleAnswerBloc(),
  );
 
  serviceLocator.registerFactory(
    () => PuzzleBloc(
      boardSize: BOARD_SIZE,
      getPuzzleTree: serviceLocator(),
      getLettersBoard: serviceLocator(),
    ),
  );
 
  serviceLocator.registerFactory(
    () => AddPacketBloc(
      localDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerFactory(
    () => SingleQuizHistoryBloc(
      getSingleQuizAnswerList: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => NavigatorManagerBloc(
      navigatorKey: serviceLocator(),
      userBloc: serviceLocator(),
      dateTimeFormatter: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => DialogManagerBloc(),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetQuizMeaningList(
      quizRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetQuizId(
      quizRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => UpdateUserExperienceAndActivity(
      repository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => PostQuizAnswer(
      answerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetAllSearchMeaningList(
      repository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetFullHistoryList(
      repository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => LogoutUserLocal(
      userRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetSingleQuizAnswerList(
      repository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => ChooseNextTraining(
      managerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => ChooseNextTrainingComplete(
      managerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => RemoveTraining(
      managerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => RemoveTrainingComplete(
      managerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => ChooseTranslationDirection(
      managerRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetCollectionList(
      collectionRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetSingleUser(
      userRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetUserLocal(
      userRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetSavedPacketList(
      packetRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetPuzzleTree(
      puzzleRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton(
    () => GetLettersBoard(
      puzzleRepository: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<QuizRepository>(
    () => QuizRepositoryImpl(
      networkInfo: serviceLocator(),
      remoteDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<AnswerRepository>(
    () => AnswerRepositoryImpl(
      networkInfo: serviceLocator(),
      remoteDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<StatsRepository>(
    () => StatsRepositoryImpl(
      networkInfo: serviceLocator(),
      remoteDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<CollectionRepository>(
    () => CollectionRepositoryImpl(
      networkInfo: serviceLocator(),
      localDataSource: serviceLocator(),
      remoteDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<UserRepository>(
    () => UserRepositoryImpl(
      networkInfo: serviceLocator(),
      localDataSource: serviceLocator(),
      remoteDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<PacketRepository>(
    () => PacketRepositoryImpl(
      localDataSource: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<PuzzleRepository>(
    () => PuzzleRepositoryImpl(),
  );
 
  serviceLocator.registerLazySingleton<StorageAnswerRepository>(
    () => StorageAnswerRepositoryImpl(),
  );
 
  serviceLocator.registerLazySingleton<QuizRemoteDataSource>(
    () => QuizRemoteDataSourceImpl(
      client: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<StatsRemoteDataSource>(
    () => StatsRemoteDataSourceImpl(
      client: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<AnswerRemoteDataSource>(
    () => AnswerRemoteDataSourceImpl(
      client: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<UserRemoteDataSource>(
    () => UserRemoteDataSourceImpl(
      client: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<UserLocalDataSource>(
    () => UserLocalDataSourceImpl(
      sharedPreferences: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<PacketLocalDataSource>(
    () => PacketLocalDataSourceImpl(
      sharedPreferences: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<CollectionRemoteDataSource>(
    () => CollectionRemoteDataSourceImpl(
      client: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<CollectionLocalDataSource>(
    () => CollectionLocalDataSourceImpl(
      sharedPreferences: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<NetworkInfo>(
    () => NetworkInfoImpl(
      serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<Experience>(
    () => ExperienceImpl(
      percentageConverter: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<AnswerKnowledge>(
    () => AnswerKnowledgeLocal(
      similarity: serviceLocator(),
      percentageConverter: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<AnswerInspector>(
    () => AnswerInspectorImpl(
      similarityCheck: serviceLocator(),
    ),
  );
 
  serviceLocator.registerLazySingleton<Similarity>(
    () => LevenshteinDistance(),
  );
 
  serviceLocator.registerLazySingleton<DateTimeFormatter>(
    () => FormatHistoryDate(),
  );
 
  serviceLocator.registerLazySingleton(() => PercentageConverter());
 
  final sharedPreferences = await SharedPreferences.getInstance();
  serviceLocator.registerLazySingleton(() => sharedPreferences);
  serviceLocator.registerLazySingleton(() => http.Client());
  serviceLocator.registerLazySingleton(() => DataConnectionChecker());
 
  serviceLocator
      .registerLazySingleton<GlobalKey<NavigatorState>>(() => GlobalKey());
}

At a certain scale manually managing all your dependencies become a nightmare.

Rest assured, a solution exists! It is called injectable.

injectable was first released on 28 January 2020. I was developing apps in Flutter way before that date: the release of this package saved the day going forward 🙏🏼

How to configure injectable

injectable is a code generator package that relies on build_runner. injectable inspects all your dependencies and automatically generates a get_it configuration, so you do not need to manually define it.

First, install injectable, injectable_generator, and build_runner:

pubspec.yaml
name: flutter_supabase_complete
description: A new Flutter project.
publish_to: "none"
 
version: 1.0.0+1
 
environment:
  sdk: ">=2.17.6 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
 
  injectable: ^1.5.3
  get_it: ^7.2.0
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  build_runner: ^2.2.0
  injectable_generator: ^1.5.4

Note: injectable relies on get_it to work, so you need to install get_it as well

Then create an injectable.dart file inside lib with the following basic configuration:

injectable.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
 
final getIt = GetIt.instance;
 
@InjectableInit()
void configureDependencies() => $initGetIt(getIt);

$initGetIt does not exists at the moment. It will be generated by injectable when running the build command:

flutter pub run build_runner build

Finally, remember to call configureDependencies during the app startup inside main.dart:

main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  // Dependency injection (injectable)
  configureDependencies();
 
  runApp(App());
}

Defining concrete implementations with injectable

Now we simply need a way to inform injectable of our classes and objects.

Easy! Based on our example, we just need to annotate our class with @Injectable:

supabase_auth.dart
@Injectable(as: AuthRepository)
class SupabaseAuth implements AuthRepository {
  final Database _database;
 
  ...
}

Now injectable will treat any instance of AuthRepository as SupabaseAuth. The same goes for Database:

sql_database.dart
@Injectable(as: Database)
class SqlDatabase implements Database { ... }

Done! injectable will automatically resolve all the dependencies for us. We now have the ease of use of get_it, without the hassle of manually organizing dependencies!


This is just an introduction on what get_it, injectable, and dependency injection can achieve for you.

There are many more usecases covered by these packages (environments, lazy singletons, factories). I suggest you to dive deeper beyond the basics to explore how injectable can help you developing your app with ease.

As always, you can follow me on Twitter at @SandroMaglione for more updates and advanced topics on Dart and Flutter.

Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.