Dependency injection is necessary if you're not coding spaghetti ? and you want to keep nice layers of separation in your Flutter app's codebase. The problem is that all of the libraries out there, such as get_it or kiwi, are just service locators with no support or a limited support for automating the registration of dependencies.
Dagger solves it elegantly for native Android and Angular is also known for its powerful dependency injection framework. Now, we Flutter developers can finally use something similar - the injectable package which is a code generator for get_it.
Starter project we all love
Yes, it's true. We're going to build yet another counter app BUT this one will be special. We won't always increment the counter by one (1), instead, based on whether we're in development environment or a production environment, the counter is going to get incremented by different values. Where are we going to get the increment values from, you ask? Well, from ICounterRepository
, of course!
In order to keep our code testable, CounterChangeNotifier
depends on an abstract class ICounterRepository
which is then in turn implemented by CounterRepository
(production) and DevCounterRepository
.
counter_change_notifier.dart
class CounterChangeNotifier extends ChangeNotifier {
final ICounterRepository _counterRepository;
CounterChangeNotifier(this._counterRepository);
int _value = 0;
int get value => _value;
void increment() {
_value += _counterRepository.getIncrement();
notifyListeners();
}
}
The problem with the starter project is that we're doing all the injection manually inside main.dart. I mean, we don't even use get_it and instead we populate the constructors completely by ourselves.
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: ChangeNotifierProvider(
// Manually passing in the production repository
create: (_) => CounterChangeNotifier(CounterRepository()),
child: CounterPage(),
),
);
}
}
While using get_it in its plain form would help with maintainability, it surely wouldn't cut down on the boilerplate... That's precisely the reason why injectable is so awesome!
Setting up injectable
The first step is obviously adding all of the packages to pubspec.yaml. In addition to injectable and its generator, we're also going to add build_runner and get_it.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
provider: ^4.0.2
injectable: ^0.1.0
get_it: ^3.1.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
injectable_generator: ^0.1.0
There's really not much actual setup code to write. Apart from annotating injectable classes, we're going to create only one setup function configureInjection
which will call a function generated by the library called $initGetIt
.
injection.dart
import 'package:injectable/injectable.dart';
import 'package:injectable_prep/injection.iconfig.dart';
@injectableInit
void configureInjection(String environment) =>
$initGetIt(environment: environment);
abstract class Env {
static const dev = 'dev';
static const prod = 'prod';
}
environment
string is what allows us to easily manage between multiple implementations of ICounterRepository
. While can be any kind of a String
, I find that creating constants is better than passing around String
literals.The @injectableInit
annotation is very important as this is what triggers code generation. In our case, the generated code will be located at injection.iconfig.dart. After running everyone's favorite command:
?? terminal
flutter pub run build_runner watch --delete-conflicting-outputs
...we can go to main.dart and call configureInjection(Env.prod)
from the main
function. Of course, this won't do anything meaningful yet as we haven't registered any classes with injectable.
main.dart
void main() {
configureInjection(Env.prod);
runApp(MyApp());
}
main
function to pass in, for example, Env.dev
.Annotating the classes
This step is what you probably came here for - you don't want to register factories and singletons manually but instead let code gen do the job for you in a fraction of the time.
Let's start off at the ICounterRepository
. We want to mark it as @injectable
and also bind it to specific implementations based on the current environment with @Bind.toType
.
i_counter_repository.dart
@Bind.toType(DevCounterRepository, env: Env.dev)
@Bind.toType(CounterRepository, env: Env.prod)
@injectable
abstract class ICounterRepository {
int getIncrement();
}
@injectable
which generates a factory, the library also provides @singleton
and @lazySingleton
annotations.Our journey with the repository doesn't end here though. We also need to mark its concrete implementations with @injectable
. Doing so will firstly add appropriate import
statements to the generated file. Secondly, we'll also be able to resolve dependencies by their concrete types under any environment.
counter_repository.dart
@injectable
class CounterRepository implements ICounterRepository {
@override
int getIncrement() => 1;
}
dev_counter_repository.dart
@injectable
class DevCounterRepository implements ICounterRepository {
@override
int getIncrement() => 2;
}
Lastly, let's annotate the CounterChangeNotifier
. In contrast with the repository, it contains a non-empty constructor. This is where the injectable package shines the most! It will figure out to pass in an argument on its own.
counter_change_notifier.dart
@injectable
class CounterChangeNotifier extends ChangeNotifier {
final ICounterRepository _counterRepository;
CounterChangeNotifier(this._counterRepository);
...
}
A look at the generated file
Boom ? We literally added a few annotations to our classes and our work is done! Opening up injection.iconfig.dart reveals an image familiar to developers used to get_it. The addition of automatic environment resolving for abstract classes is ???
injection.iconfig.dart
final getIt = GetIt.instance;
void $initGetIt({String environment}) {
getIt
..registerFactory<CounterChangeNotifier>(
() => CounterChangeNotifier(getIt<ICounterRepository>()))
..registerFactory<CounterRepository>(() => CounterRepository())
..registerFactory<DevCounterRepository>(() => DevCounterRepository());
if (environment == 'dev') {
_registerDevDependencies();
}
if (environment == 'prod') {
_registerProdDependencies();
}
}
void _registerDevDependencies() {
getIt..registerFactory<ICounterRepository>(() => DevCounterRepository());
}
void _registerProdDependencies() {
getIt..registerFactory<ICounterRepository>(() => CounterRepository());
}
Using get_it to resolve dependencies
Having everything else in place, we just need to get rid of the manual instantiation in main.dart and use the provided GetIt
instance instead.
main.dart
void main() {
configureInjection(Env.prod);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: ChangeNotifierProvider(
create: (_) => getIt<CounterChangeNotifier>(),
child: CounterPage(),
),
);
}
}
When we run the app, the counter will be incremented by 1 since we're running under the production environment, as specified by Env.prod
. Changing it to Env.dev
...
... and then hot restarting will make the counter be incremented by 2.
I know this is just a silly counter app example but you can probably imagine that the production environment would communicate with production APIs containing actual user data while the development environment would operate with something where it's safe to do silly things like deleting everything from the database without having a backup ?
Bonus: Test environment
Doing test-driven development or just testing after the code has been written (?) highly benefits from mocks. If you've ever mocked something, you know how time-consuming it is to create mock classes manually. Thankfully injectable and its environments can help even with testing. First, let's add mockito into pubspec.yaml.
pubspec.yaml
dev_dependencies:
...
mockito: ^4.1.1
Add an additional test environment to the Env
abstract class.
injection.dart
abstract class Env {
static const test = 'test';
static const dev = 'dev';
static const prod = 'prod';
}
Create a mock_counter_repository.dart file next to all of the other repository implementation files and mark it as @injectable
.
mock_counter_repository.dart
@injectable
class MockCounterRepository extends Mock implements ICounterRepository {}
Lastly, bind the abstract ICounterRepository
to the MockCounterRepository
"implementation" under the test environment.
i_counter_repository.dart
@Bind.toType(MockCounterRepository, env: Env.test)
@Bind.toType(DevCounterRepository, env: Env.dev)
@Bind.toType(CounterRepository, env: Env.prod)
@injectable
abstract class ICounterRepository {
int getIncrement();
}
Gone are the days of constantly defining mocks throughout multiple test files! Now it's enough to call configureInjection(Env.test);
from within the setUpAll
callback and you can resolve dependencies just like in your regular non-test code.
example_test.dart
void main() {
setUpAll(() {
// Just like in the regular main() function inside main.dart
configureInjection(Env.test);
});
test(
"should do something",
() async {
// arrange
final mockCounterRepository = getIt<ICounterRepository>();
when(mockCounterRepository.getIncrement()).thenReturn(123);
// act
// TODO: Some action here
// assert
verify(mockCounterRepository.getIncrement()).called(10);
},
);
}
The amount of boilerplate and maintenance nightmare that's prevented by using the injectable package is totally incredible. You're literally one or two simple annotations away from having a robust dependency injection system working with the battle-tested get_it package in place. Use it and save your time!
This helps so much. In one of my app, the function to register services to GetIt is so long that it is becoming difficult to add new or update existing services.
Thank you very much for your tutorials, which helped me a lot to dive into the flutter technology stack. However, I would like to note that binding the implementation to the abstraction is a code smell and the injectable package has been updated to change that.
I would suggest changing the tutorial or taking it offline, as it not only shows an outdated state of the package, but also shows a bad programming style, in my opinion.
Absolutely, I’ll need to get around to updating some of the outdated tutorials.
can i ask one help….
now, after one month; injection.iconfig.dart is product with different code from descriprion upstair……… i am not so much expert in exactly that argument and i dont know how to set injection.dart for new modify:
now flutter pub run build_runner watch –delete-conflicting-outputs go out with that:
void $initGetIt(GetIt g, {String environment}) {
g.registerFactory(
() => CounterChangeNotifier(g()));
g.registerFactory(() => CounterRepository());
g.registerFactory(() => DevCounterRepository());
}
there is Getit more…. and offcourse injection.dart is in error; in particular: $initGetIt(environment: environment);
i need to write some Getit something like parameter
thankYouVeryMuchAndVeryGoodWorkAndiWilltryToFollowAllYourLessons:)
Hello,
I am practicing this but while using @Bind annotation it gives me the error as follows :
Undefined name ‘Bind’.
Try correcting the name to one that is defined, or defining the name.dartundefined_identifier
Please help me with this.
use @Injectable(as: RepositoryInterface)
I has one problem with injection
I need to pass a string ‘variable’ from one domain bloc to other domain bloc to use in other domain repo!
How can i do that? Plz help
Hey, thanks a lot for the tuts they’ve helped me lots of back pain. I have had some problems with the Signinbloc in my case the authFacade is null when the signinBtn is pressed. I’m using a rest api but it does execute. I have used getIt and injectable and all seems okay
Hi Matt,
thanks alot for your tutorials, I am a big fan and I have studied almost 70% of them. For this tutorial I am getting lost as it is outdated and I cant figure out how to go around it. Plus there isn’t enough examples on the pub.dev or stack.
Can you upgrade this tutorial to reflect the new changes?
I keep getting this error:
[SEVERE] injectable_generator:injectable_builder on lib/presenter/bloc/counter_value_notifier.dart (cached): Failed assertion: boolean expression must not be null
I don’t want to clutter the comments section but if anybody can help I will really appreciate it.
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?
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
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?