Approaches to dependency injection in Flutter
Published: 2022-05-06 08:12:51

---
title: Approaches to dependency injection in Flutter
published: true
date: 2022-05-06 08:07:23 UTC
tags: 
canonical_url: https://ptrbrynt.com/approaches-to-dependency-injection-in-flutter
---

Any object-oriented software project worth it's salt should adopt a dependency injection pattern. But when it comes to Flutter apps, there are far too many options. So what do all these options look like, and which is best?

### Side-note: on state management

A lot of the packages and techniques we're covering here cross over into the world of State Management. But dependency injection and state management are **not** the same thing, and shouldn't be considered as such.

_State management_ is strictly to do with the state of your app's user interface. Should this button be enabled? What's the content of this text field? What page are we on right now?

_Dependency injection_ is concerned with providing each class within your app with the dependencies it needs to function e.g. a Bloc class depending on a Repository.

As it happens, there are a lot of Flutter packages which advertise themselves as _both_ state management packages and dependency injection frameworks. They're not wrong, but this can sometimes cause some confusion when developers conflate the two patterns.

## What are we trying to achieve?

Dependency injection can be achieved without any special packages or techniques. Consider this class:

```dart
class ClassA {
  // Some implementation...
}

class ClassB {
  final objectA = ClassA();

  // Some implementation...
}
```

Here, we have two classes: `ClassA`, and `ClassB`. `ClassB` includes an instance of `ClassA` as part of its implementation; in other words, `ClassB` **depends on** `ClassA`.

However, the code above has some problems.

Let's say we change the implementation of `ClassA` such that its constructor changes. We now have to go through our code and find every instance of `ClassA` and change its constructor call. That's hugely tedious and will very quickly become impossible to maintain.

Another issue is to do with testing. In our implementation above, `ClassB` is deciding exactly what version of `ClassA` it will use. So what about when we want to write unit tests for `ClassB`? We will also find ourselves testing the implementation of `ClassA`, which will add lots of complexity to our tests and ultimately make them less useful.

We can solve these problems using the Dependency Injection pattern, which simply involves providing a class's dependencies via its constructor:

```dart
class ClassB {
  ClassB(this.objectA);

  final ClassA objectA;
}
```

Now, we can pass whatever object we want as `objectA`, including a mocked version of `ClassA`. It also ensures that if the constructor for `ClassA` changes, the implementation of `ClassB` doesn't need to change too. This improves the separation of concerns and makes `ClassB` independently testable.

We can even take this one step further by extracting `ClassA`'s interface into an abstract version:

```dart
class ClassB {
  ClassB(this.objectA);

  final InterfaceA objectA;
}
```

This ensures that `ClassB` is entirely independent of the underlying implementation of `InterfaceA`, which further decreases coupling, and achieves [dependency inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle).

## We don't actually _need_ frameworks

The obvious conclusion of the definition above is that dependency injection is astonishingly simple. So why are there so many frameworks and packages for it?

The answer is largely about hiding complexity for developers. In large applications, the relationships between classes can become pretty complex. There are a few challenges in particular for Flutter apps:

1. Environments – how can I make it easy to switch between staging/test dependencies and production ones?

2. Scopes – how can I control which dependencies should be available to which dependent classes? How do I know whether or not I'm missing a dependency in a given scope?

3. Lifecycles – how can I understand and control when dependencies should be created, recreated, or destroyed?

A handful of frameworks and patterns exist, and each of them approaches the three concerns above in a slightly different way. I'm going to compare two extremely popular packages which represent the two most common approaches to dependency injection.

## Provider

[Provider](https://pub.dev/packages/provider) is an extremely popular library/pattern in the Flutter world which takes advantage of Flutter's `InheritedWidget` to provide dependencies via the widget tree.

This makes it astonishingly simple to manage dependencies. All you have to do is wrap a widget with a `Provider`:

```dart
Provider<ClassA>(
  create: (context) => ClassA(),
  child: MyApp(),
);
```

Now, any child of the `MyApp` widget can access the instance of `ClassA` like this:

```dart
context.watch<ClassA>(); // Makes the widget listen to changes in `ClassA` and rebuild
context.read<ClassA>(); // Returns the instance of `ClassA` without listening to it
```

So how well does Provider address the 3 concerns above?

1. Environments: Provider doesn't have any built-in mechanism for environment management, which gives you the freedom to manage environment variables however you like. You could have different `main.dart` files for each environment, or use Dart's `fromEnvironment` methods to read variables passed in via the `flutter build` and `flutter run` commands. It's up to you!

2. Scopes: You can scope your dependencies using Provider by only wrapping the widgets whose children need access to the given dependency. This makes a lot of sense for Flutter apps! A couple of disadvantages do exist though:

3. Lifecycles: You get quite a lot of flexibility when it comes to deciding when objects are created by Providers. You can either let the Provider manage creation for you (and you get to decide whether it does lazy instantiation or not), or you can manage the creation of the dependency yourself and use a `Provider.value` constructor to inject the object into the widget tree. This flexibility covers most requirements you might have when it comes to dependency lifecycle management.

There are some other things to consider when using Provider:

- There is a limitation in that you can only have one Provider per type in your Widget tree. If you have more than one `Provider<ClassA>`, then `context.read` will just retrieve the closest one. This doesn't usually cause too many problems but is something to be aware of.
- Provider's error messaging is really helpful, which is a bonus when it comes to debugging
- Provider is the preferred dependency injection solution for the Bloc library, if you like to use that for state management

Overall, I really like the flexibility and simplicity of Provider!

## GetIt + Injectable

[GetIt](https://pub.dev/packages/get_it) describes itself as a service locator for Dart, and when paired with [Injectable](https://pub.dev/packages/injectable) it becomes a full dependency injection framework, with lots of similarities to Dagger/Hilt from the Android world.

Injectable relies on code generation to automatically write initialization code for GetIt. You can create an “injectable” class by simply annotating it with `@injectable` (or `@singleton`).

Instances of dependencies can then be accessed using this syntax: `GetIt.I<ClassA>()`. Notice that there's no dependency on a `BuildContext` here; unlike Provider, GetIt dependencies exist _outside_ the widget tree.

How does it handle our three concerns?

1. Environments: Injectable includes great support for managing environments. You can create a number of `Environment` objects and use them to annotate your dependencies (e.g. `@staging`, `@production`). You then specify which environment you want when initializing GetIt. This is really intuitive!

2. Scope: Scope is less easy to manage with GetIt. It's all manual so you would need to remember to reset your scopes when required according to events taking place in your app. This is easy to mess up and difficult to test.

3. Lifecycles: You do get some flexibility when it comes to lifecycles with GetIt/Injectable. You can choose whether dependencies are created fresh each time they're requested (a factory), or whether the same instance is passed on each request (a singleton). You can also choose whether dependencies are initialized lazily or not. You also have some control over when singletons are invalidated and need to be recreated. But the documentation around this isn't very comprehensive.

Overall, I'm not a huge fan of this approach for one reason: having global access to any dependency feels like a bit too much power to put in the hands of your team! It's far too easy to break conventions around the hierarchy of dependencies when you can just pull in whatever dependency you like from anywhere in your app. And while I'm not against code generation in principle, this doesn't feel like a problem that needs to be solved with more code generation.

_Lots of other packages exist which are extremely similar to GetIt/Injectable, including Kiwi, Injector, Scope, and Stark. These are all different variations on the same idea of using a “container” to store and provide dependencies from outside the widget tree, and they all have the same advantages and drawbacks._

## Packages we haven't talked about

- [Riverpod](https://pub.dev/packages/riverpod), which is **not** a DI framework but is often mistaken for one.
- [Bloc](https://pub.dev/packages/bloc). Again, not a DI framework but does include Provider-style dependency injection features. You can use Bloc without using Provider, but I think it works best with the Provider-style approach.

## Which approach is better?

Personally, having worked with GetIt/Injectable for a very long time and experimented with Provider more recently, I'm leaning very strongly towards the Provider-style approach to dependency management. It feels much more Flutter-native, and it's much more transparent in terms of how scopes and lifecycles are managed. That said, I can see it getting tedious when a project gets really big, and Injectable's code generation would certainly solve the complexity problem for larger projects.

What do you think? Are there any frameworks or approaches I've missed? Do you agree that Provider is generally the better approach for Flutter apps?