Skip to content

Dependency Injection

Zubair Rehman edited this page Jun 16, 2019 · 3 revisions

Inject

Compile-time dependency injection for Dart and Flutter, similar to Dagger.

Installation

As there’s no package in official repository, we have to install it manually. I prefer to do it as a git package, so I’m going to define its dependencies in pubspec.yaml file

dependencies:
  inject:
    git:
      url: https://github.com/google/inject.dart.git
      path: package/inject

dev_dependencies:
  build_runner: ^1.3.0

  inject_generator:
    git:
      url: https://github.com/google/inject.dart.git
      path: package/inject_generator

Usage

What functionality do we usually expect from a DI library? Let’s go through some common use cases:

Concrete class injection

It can be as simple as this:

import 'package:inject/inject.dart';

@provide
class StepService {
  // implementation
}

We can use it e.g. with Flutter widgets like this:

@provide
class SomeWidget extends StatelessWidget {
  final StepService _service;

  SomeWidget(this._service);
}

Interface injection

First of all we need to define an abstract class with some implementation class, e.g.:

abstract class UserRepository {
  Future<List<User>> allUsers();
}

class FirestoreUserRepository implements UserRepository {
  @override
  Future<List<User>> allUsers() {
    // implementation
  }
}

And now we can provide dependencies in our module:

import 'package:inject/inject.dart';

@module
class UsersServices {
  @provide
  UserRepository userRepository() => FirestoreUserRepository();
}

Providers

What to do if we need not an instance of some class to be injected, but rather a provider, that will give us a new instance of this class each time? Or if we need to resolve the dependency lazily instead of getting concrete instance in constructor? I didn’t find it neither in documentation (well, because there’s no documentation at all) nor in provided examples, but it actually works this way that you can request a function returning the required instance and it will be injected properly.

We can even define a helper type like this:

typedef Provider<T> = T Function();

and use it in our classes:

@provide
class SomeWidget extends StatelessWidget {
  final Provider<StepService> _service;

  SomeWidget(this._service);

  void _someFunction() {
    final service = _service();
    // use service
  }
}

Assisted injection

There’s no built in functionality to inject objects that require arguments known at runtime only, so we can use the common pattern with factories in this case: create a factory class that takes all the compile-time dependencies in constructor and inject it, and provide a factory method with runtime argument that will create a required instance.

Singletons, qualifiers and asynchronous injection

Yes, the library supports all of this. There’s actually a good explanation in the official example.

Wiring it up

The final step in order to make everything work is to create an injector (aka component from Dagger), e.g. like this:

import 'main.inject.dart' as g;

@Injector(const [UsersServices, DateResultsServices])
abstract class Main {
  @provide
  MyApp get app;
  static Future<Main> create(
    UsersServices usersModule,
    DateResultsServices dateResultsModule,
  ) async {
    return await g.Main$Injector.create(
      usersModule,
      dateResultsModule,
    );
  }
}

Here UserServices and DateResultsServices are previously defined modules, MyApp is the root widget of our application, and main.inject.dart is an auto-generated file (more on this later).

Now we can define our main function like this:

void main() async {
  var container = await Main.create(
    UsersServices(),
    DateResultsServices(),
  );
  runApp(container.app);
}

Running

As inject works with code generation, we need to use build runner to generate the required code. We can use this command:

flutter packages pub run build_runner build delete-conflicting-outputs

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

flutter packages pub run build_runner watch

But there’s one important moment here: by default the code will be generated into the cache folder and Flutter doesn’t currently support this (though there’s a 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 file injection/inject.dart/package/inject_generator/build.yaml except for one line: build_to: cache has been replaced with build_to: source.

Now we can run the build_runner, it will generate the required code (and provide error messages if some dependencies cannot be resolved) and after that we can run Flutter build as usual.

Useful Resources:

You should also check the examples provided with the library itself, and if you have some experience with Dagger library, inject will be really very familiar to you.

Clone this wiki locally