Most of you are probably aware of packages like get_it or even provider which are amazing tools for doing basic and manual dependency injection. When your app is small, filling in a all of the constructor parameters yourself may not seem to be such a daunting task. Once your app starts to grow though and, as a proper programmer, you want to keep it clean by creating smaller sensible classes, doing it all by hand becomes a nightmare.
In this tutorial, you're going to learn how to utilize the power of code generation together with the kiwi package to configure your dependency injection container in no time.
Our starting point
Just so that we have a substantial amount of classes with dependencies, I created a starter project (get it from the link above!) with one simple feature - to get weather forecast. Following the principles of clean architecture, we have beautiful classes separated into 3 layers - data, domain and presentation.
No matter if you're proficient in clean architecture or if you've never even heard of it (???), there's a lot of dependencies! Luckily for us all, the starter project is written with loose coupling in mind, so dependency injection will be no problem.
Apart from dependency injection itself, the starter project is fully functional. When you run it, the app will show two segments of weather forecast in a totally wonderful ? way:

The app displays two weather forecasts
At this point, all of the classes are constructed from the WeatherForecastWidget
where we kick off a ChangeNotifierProvider
.
weather_forecast_widget.dart
...
return ChangeNotifierProvider(
// Resolving the whole dependency graph directly from the UI
builder: (context) => WeatherForecastChangeNotifier(
GetForecast(
ForecastRepositoryImpl(
FakeForecastDataSource(),
),
// Instance just for testing, you'd normally
// want to have a LocationRepository here
Location(latitude: 0, longitude: 0),
),
),
...
This code is just not good. Not only we're resolving all the dependencies manually, but we're doing that from within a widget class! In short, the code above will benefit from having a centralized place where the dependencies get injected.
Adding Kiwi
As I said at the beginning, we could go the manual route and resolve all the constructor parameters by writing repetitive code. Had we used get_it, or provider for injecting dependencies, we wouldn't have any other option. Kiwi, on the other hand, offers a different solution.
While you can certainly use kiwi manually and resolve everything yourself, there's also a kiwi_generator package, which allows you to go from this...
injector.dart
container.registerFactory<ForecastRepository, ForecastRepositoryImpl>(
(c) => ForecastRepositoryImpl(c.resolve()),
);
...to this:
injector.dart
@Register.factory(ForecastRepository, from: ForecastRepositoryImpl)
Notice that we only have to instruct the kiwi_generator about the types which we register but otherwise, we don't have to deal with any minutia, such as resolving constructor parameters manually. That's done by the generated code.
Before writing any of this, it makes sense to add the packages into our project.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
provider: ^3.1.0+1
kiwi: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
kiwi_generator: ^0.4.0
Setting up Kiwi for code generation
We will want to center everything related to dependency injection into an Injector
class. Firstly, let's add the basic setup code which is more or less needed no matter if we use the generator or not.
injector.dart
import 'package:kiwi/kiwi.dart';
part 'injector.g.dart';
abstract class Injector {
static Container container;
static void setup() {
container = Container();
_$Injector()._configure();
}
// For use from classes trying to get top-level
// dependencies such as ChangeNotifiers or BLoCs
static final resolve = container.resolve;
void _configure() {
// Configure modules here
}
}
We will want to call setup()
as soon as possible in order to kick off resolving our to-be-added dependencies. Let's add this call to the main()
method in main.dart, so that we don't forget about it later.
main.dart
void main() {
Injector.setup();
runApp(MyApp());
}
Registering dependencies
To use kiwi_generator for populating the constructor parameters, you need to annotate a method either with @Register.factory
or @Register.singleton
. These annotations will be picked up by source gen and translated to whatever the code would be if we wrote it manually.
While nothing prevents you from putting all of these annotations on just the main _configure()
method, you should strive to keep your code clean. That's why we're going to create modules! If you're following clean architecture, these modules will be equivalent to features.
Every module has separate methods for:
- Registering factories (if any)
- Registering singletons (if any)
- Registering instances (if any)
- A collective method which calls the ones above
Weather module
In the case of our simple app, we will want to "kiwify" what we're currently doing by directly instantiating the classes. If you need a refresher, this is how the dependency tree looks like:
weather_forecast_widget.dart
...
return ChangeNotifierProvider(
// Resolving the whole dependency graph directly from the UI
builder: (context) => WeatherForecastChangeNotifier(
GetForecast(
ForecastRepositoryImpl(
FakeForecastDataSource(),
),
// Instance just for testing, you'd normally
// want to have a LocationRepository here
Location(latitude: 0, longitude: 0),
),
),
...
WeatherForecastChangeNotifier
, GetForecast
, ForecastRepository
and ForecastDataSource
will be registered as factories. The only remaining class, Location
, will get registered as an instance for a very simple reason - we need to hard code its constructor parameters.
The only thing which kiwi_generator cannot process are instances, because of course, it doesn't know what kind of custom constructor parameters we want to pass in. All of the factories though are handled by only specifying the type of the class we want to register. Resolving constructor parameters is left for the generator.
injector.dart
...
//! WeatherForecastFeature
void _configureWeatherForecastFeatureModule() {
_configureWeatherForecastFeatureInstances();
_configureWeatherForecastFeatureFactories();
}
void _configureWeatherForecastFeatureInstances() {
// The default Location instance used unless we specify a name
container.registerInstance(
Location(latitude: 0, longitude: 0),
);
// Used only when we specify the name 'London'
container.registerInstance(
Location(latitude: 51.5073, longitude: -0.1277),
name: 'London',
);
}
@Register.factory(WeatherForecastChangeNotifier)
// If we didn't add the resolvers map with Location being resolved to 'London',
// we'd get the default Location instance (lat: 0, lon: 0)
@Register.factory(GetForecast, resolvers: {Location: 'London'})
// Abstract class being resolved to a concrete implementation
@Register.factory(ForecastRepository, from: ForecastRepositoryImpl)
@Register.factory(ForecastDataSource, from: FakeForecastDataSource)
void _configureWeatherForecastFeatureFactories();
...
After running build_runner's build command, an injector.g.dart file will be generated with all of the constructor parameters resolved for us.
The only thing left for us to do now in the injector.dart file is to sort of "register" even this whole WeatherForecastFeature module by including it in the main _configure()
method.
injector.dart
...
void _configure() {
_configureWeatherForecastFeatureModule();
// Configure other modules here
}
...
Resolving the top-level dependency
Great! We're already calling Injector.setup()
from the main()
method, so the only remaining step is to replace the manual dependency-resolving monstrosity inside the WeatherForecastWidget
.
With kiwi, or really any other DI library, it's enough to resolve only the top-level dependency and all of its children will be resolved automatically. Our top-level dependency is the WeatherForecastChangeNotifier
.
injector.dart
class WeatherForecastWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => Injector.resolve<WeatherForecastChangeNotifier>(),
child: Consumer<WeatherForecastChangeNotifier>(
builder: (context, provider, _) {
...
And now, enjoy running the app knowing that you're using a neat generated dependency injection (or rather a generated service locator) that allows you to omit all those redundant parameters upon registration.
is it compile time dependency injection?? or it is kind of service locator?
Also can you make a tutorial on inject.dart??? I read somewhere that it is best DI framework for flutter because it is compile time DI.
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?