Dependency Injection in Flutter made easy — inject.dart package

Loredana Zdrânc
Zipper Studios
Published in
7 min readNov 12, 2019

As a Flutter developer, do you ever wonder how to easily write testable code or implement a loosely coupled app? Dependency Injection is the answer. In this article, you will find a short definition of dependency injection, its advantages and how you can implement it in Flutter using the inject.dart library located in the Google GitHub repository. Before proceeding, keep in mind that this is not an official library from Google and no support is currently provided.

This library is currently offered as-is (developer preview) as it is open-sourced from an internal repository inside Google. As such we are not able to act on bugs or feature requests at this time.

What is Dependency Injection?

Let’s get a real-world common example: a car. Do you know how a car is manufactured? To build a car you need multiple components such as wheels, a battery, an internal combustion engine, an electric generator, and so on. All these components are not built in the same factory, and in most cases not even in the same country. But, finally, all these individual components are assembled together in one factory. That’s what Dependency Injection is. The assembling factory was unaware of the build process of these different components. The only job of the factory was to assemble these components to build a car. This way the factory doesn’t need to worry about the build process of these components and can only focus on its job.

What are the benefits of Dependency Injection?

  1. Adding new features to the already existing app becomes easier.
  2. Enables loose coupling.
  3. The Unit testing process is faster and easier.
  4. Reduces boilerplate code.

Flutter, please!!!

Working with Dependency Injection in Flutter is not a challenge. There are many libraries built to integrate this pattern into your apps such as get_it, kiwi, dependencies_flutter or you can use the InheritedWidget. All these are promising libraries (or widgets, InheritedWidget is not a library) and I suggest you try each one and see which one fits your needs better. In this article, we will use a non-official library, inject.dart. Maybe you're wondering why I chose to use a non-official library when there are many other libraries available. The answer is simple: this library is heavily inspired by the Dagger library. Since I used the latter one in most of the Android projects I've worked on, I find it easier to work with inject.dart.

The inject.dart library uses code generation and is based on annotations. Its annotations are:

  • @provide: Annotation for a method (in an [Injector] or [module]), class, or constructor that provides an instance.
  • @module: Annotates a class as a collection of providers for dependency injection.
  • @injector: An annotation to mark something like an [Injector] with no included modules.
  • @singleton: An injectable class or module provider that provides a single instance.
  • @asynchronous: Annotates a module provider method that returns a Future.

Code, please!!!

We will update the weather app built in the previous article with Flutter. I built this app to help Flutter developers understand how they can fetch data from the API, how they can use reactive programming and how they can organize their apps using BLoC pattern. In order to proceed with this project, you have two options: go back to the previous article and follow all steps to build the weather app or, if you're already familiar with API, Rx and BLoC concepts, download the app from Github repository and continue to the next steps.

Step1. Install inject.dart into your app. As there’s no package in the official repository, we have to install it manually. I prefer to do it as a git submodule, so I’m creating a folder vendor in my project source directory before running the following command from this directory:

git submodule add https://github.com/google/inject.dart

Now we can set it up by adding the following lines into pubspec.yaml and running flutter packages get command in the terminal.

dependencies:
// other dependencies here
inject:
path: ./vendor/inject.dart/package/inject
dev_dependencies:
// other dependencies here
inject_generator:
path: ./vendor/inject.dart/package/inject_generator

Step 2. When implementing dependency injection, the first essential step is to decide what dependencies you want to inject. In this application, we propose to provide dependencies for ApiProvider, Repository and WeatherBloc classes. Why? Well, we need a WeatherBloc instance when the weather screen is built and if we build an instance of WeatherBloc inside the WeatherScreen class, we enable tight coupling which is not a good approach in Object-Oriented Programming. Then, we need the same instance of the Repository each time the WeatherBloc is built. It’s not memory-efficient to build a Repository object each time a new instance of WeatherBloc is provided. That’s why we'll use the @singleton annotation for the Repository class. The same reasoning is also applied to the ApiProvider class. For all these classes we have to use the @provide annotation because they should enter into the dependency graph and their constructor’s arguments are injected when the classes are injected. Update the corresponding Dart files of these classes as shown below:

  • repository.dart
import 'package:inject/inject.dart';
import 'package:weather_app_di/model/weather_response_model.dart';
import 'api_provider.dart';

@provide
@singleton
class Repository {
ApiProvider apiProvider;

Repository(this.apiProvider);

Future<WeatherResponse> fetchLondonWeather() => apiProvider.fetchLondonWeather();
}
  • api_provider.dart
import 'dart:convert';
import 'package:http/http.dart' show Client;
import 'package:inject/inject.dart';
import 'package:weather_app_di/model/weather_response_model.dart';

@provide
@singleton
class ApiProvider {
Client client = Client();
final _baseUrl =
"https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22";

Future<WeatherResponse> fetchLondonWeather() async {
final response = await client.get("$_baseUrl");

print(response.body.toString());

if (response.statusCode == 200) {
return WeatherResponse.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load weather');
}
}
}
  • weather_bloc.dart
import 'package:inject/inject.dart';
import 'package:rxdart/rxdart.dart';
import 'package:weather_app_di/model/weather_response_model.dart';
import 'package:weather_app_di/persistence/repository.dart';

@provide
class WeatherBloc {
Repository _repository;
WeatherBloc(this._repository);
final _weatherFetcher = PublishSubject<WeatherResponse>();

Observable<WeatherResponse> get weather => _weatherFetcher.stream;
fetchLondonWeather() async {
WeatherResponse weatherResponse = await _repository.fetchLondonWeather();
_weatherFetcher.sink.add(weatherResponse);
}
dispose() {
_weatherFetcher.close();
}
}

As you can see, the Repository class does not create an instance of the ApiProvider. Its constructor has an argument of this type and this argument will be injected when the Repository class will be injected. This logic is also applied to the WeatherBloc class whose constructor has a Repository type argument that will be injected when the WeatherBloc class will be injected.

Step 3. Create a new directory inside the lib folder, called di, and add a new dart file inside, app_injector.dart. Put the following code inside the newly created file:

import 'package:inject/inject.dart';
import 'package:weather_app_di/persistence/repository.dart';
import 'package:weather_app_di/ui/weather_screen.dart';
import '../main.dart';
import 'app_injector.inject.dart' as g;

@Injector()
abstract class AppInjector {
@provide
MyApp get app;

@provide
Repository get repository;

@provide
WeatherScreen get weatherScreen;

static Future<AppInjector> create() {
return g.AppInjector$Injector.create();
}
}

Here is where the magic happens! I like to think of the Injector class as the core of Dependency Injection. The AppInjector abstract class is our injector. It provides the MyApp instance. The framework generates a concrete class AppInjector$Injector that has a static asynchronous function named create, which returns a Future<AppInjector>.

Step 3. Update the WeatherScreen class as shown below:

@provide
class WeatherScreen extends StatefulWidget {
WeatherBloc bloc;

WeatherScreen(this.bloc);

@override
WeatherScreenState createState() => WeatherScreenState();
}

class WeatherScreenState extends State<WeatherScreen> {
@override
Widget build(BuildContext context) {
widget.bloc.fetchLondonWeather();
return StreamBuilder(
stream: widget.bloc.weather,
builder: (context, AsyncSnapshot<WeatherResponse> snapshot) {
if (snapshot.hasData) {
return _buildWeatherScreen(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
});
}
//the same methods here for building UI
}

Step 4. Update the main. dart file:

import 'package:flutter/material.dart';
import 'package:weather_app_di/ui/weather_screen.dart';
import 'di/app_injector.dart';
import 'package:inject/inject.dart';

typedef Provider<T> = T Function();

void main() async {
final container = await AppInjector.create();
runApp(container.app);
}

@provide
class MyApp extends StatelessWidget {
final Provider<WeatherScreen> weatherScreen;

MyApp( this.weatherScreen) : super();

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: weatherScreen()),
);
}
}

First, we transformed the main method to work asynchronously. That’s because we want to suspend its execution until AppInjector.create() completes. Second, the WeatherScreen cannot start its build() method before the WeatherBloc is injected. The WeatherScreen will be injected when MyApp will be injected, that’s why MyApp must have the @provide annotation and a weatherScreen argument in the constructor.

Step 5. Add the inject_generator.build.yaml file in your project. Why? By default, the code will be generated into the cache folder and Flutter doesn’t currently support this (although there’s work in progress in order to solve this problem), so we need to add the file inject_generator.build.yaml with the following content:

builders:
inject_generator:
target: ":inject_generator"
import: "package:inject_generator/inject_generator.dart"
builder_factories:
- "summarizeBuilder"
- "generateBuilder"
build_extensions:
".dart":
- ".inject.summary"
- ".inject.dart"
auto_apply: dependents
build_to: source

It’s actually the same content as in the file vendor/inject.dart/package/inject_generator/build.yamlexcept for one line: build_to: cache that has been replaced with build_to: source.

Step 6. Run your app! As inject is based on code generation, we need to use build_runner to generate the required code. Add the following dependency inside the pubspec.yaml file and run the flutter packages getcommand in the terminal.

dev_dependencies:
// other dependencies here
build_runner: ^1.0.0

Now we can run the build_runner with this command:

flutter packages pub run build_runner build

or watch command in order to keep the source code synced automatically:

flutter packages pub run build_runner watch

These commands will generate the required code (and provide error messages if some dependencies cannot be resolved). After that, we can run Flutter build as usual.

Weather app

That’s all! You took a big step in learning Flutter today! Dagger can be a controversial topic for developers, but its complexity and effectiveness are peerless. I suggest you continue the exercise with inject.dart and if you get stuck somewhere in the process, post a comment below. I am more than happy to help you at any given moment. I also encourage you to try the libraries mentioned above and make Dependency Injection your friend. If you found this tutorial useful, you already know what to do: hit that clap button and follow me to get more articles and tutorials on your feed.

The full code is available on Github.

Thanks for reading!

https://www.zipperstudios.co

Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.

--

--