Flutter Generated Dependency Injection – Kiwi Tutorial

7  comments

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.

The most important principle of loose coupling is structuring your class dependencies in such a way, that they're passed in through the constructor.

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.

Learn more about manual registration with kiwi from the official docs.

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:

  1. Registering factories (if any)
  2. Registering singletons (if any)
  3. Registering instances (if any)
  4. A collective method which calls the ones above
Factories get called every time the dependencies get resolved.
Singletons get called only the first time the dependency resolution is kicked off.
Instances are, well, instances. They get called only once and immediately.

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.

Get a code snippet from creating kiwi modules here. Put it into the dart.json snippet file, which you can open from File -> Preferences -> User Snippets.

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();
...
As you can see with the instances, you can always specify a name when registering a class. This is useful as you can quickly switch between a production class and a mock class, if that's something you like to do as you're testing the app.

After running build_runner's build command, an injector.g.dart file will be generated with all of the constructor parameters resolved for us.

flutter packages pub run build_runner build

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.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter Developer at LeanCode and a developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

Flutter UI Testing with Patrol

Flutter UI Testing with Patrol
  • 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.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >