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):
What is the benefit here? Using an abstract class we can now be sure that every class that implementsAuthRepository 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 implementsAuthRepository is guaranteed to have at leastsignIn, 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 implementsAuthRepository, we can swap different implementations as we need.
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:
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:
Then we need to register the concrete implementation of AuthRepository during the app startup. We can do that inside main:
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:
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:
We can then create different implementations of Database:
Database now becomes a dependency of a concrete implementation of AuthRepository:
We have a new problem: we need to provide (inject) the Database dependency inside AuthRepository. Using get_it we have the following:
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:
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):
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:
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:
$initGetIt does not exists at the moment. It will be generated by injectable when running the build command:
Finally, remember to call configureDependencies during the app startup inside main.dart:
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:
Now injectable will treat any instance of AuthRepository as SupabaseAuth. The same goes for 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.
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.