-
Notifications
You must be signed in to change notification settings - Fork 912
Dependency Injection
Compile-time dependency injection for Dart and Flutter, similar to Dagger.
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
What functionality do we usually expect from a DI library? Let’s go through some common use cases:
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);
}
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();
}
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
}
}
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.
Yes, the library supports all of this. There’s actually a good explanation in the official example.
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);
}
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.
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.