We have all of the individual pieces of the app architecture in place. Before we can utilize them by building a UI though, we have to connect them together. Since every class is decoupled from its dependencies by accepting them through the constructor, we somehow have to pass them in.
We've been doing this all along in tests with the mocked classes. Now, however, comes the time to pass in real production classes using a service locator.
Injecting Dependencies
Almost every class we created in this course until now has some dependencies. Even in a small app like the Number Trivia App we're building, there's quite a lot to configure. There are multiple service locators suitable for Flutter and as you may remember from the 2nd part, we're using get_it.
We will now go over all the classes from the top of the "call flow" downward. In our case, this means starting from the NumberTriviaBloc
and ending with the external dependencies like the SharedPreferences
.
Let's set everything up in a new file called injection_container.dart located directly inside the root lib folder. In essence, we're going to fill in the constructors with appropriate arguments. The basic structure of the file is the following:
injection_container.dart
final sl = GetIt.instance;
void init() {
//! Features - Number Trivia
//! Core
//! External
}
init()
functions per every feature just to keep things organized.init()
functions from within the main one.The init()
function will be called immediately when the app starts from main.dart. It will be inside that function where all the classes and contracts will be registered and subseqently also injected using the singleton instance of GetIt
stored inside sl
(that's short for a service locator).
The get_it package supports creating singletons and instance factories. Since we're not holding any state inside any of the classes, we're going to register everything as a singleton, which means that only one instance of a class will be created per the app's lifetime. There will be only one exception to this rule - the NumberTriviaBloc
which, following the "call flow", we're going to register first.
Registering a Factory
The registration process is very straightforward. Just instantiate the class as usual and pass in sl()
into every constructor parameter. As you can see, the GetIt
class has the call()
method to make for an easier syntax, very much like our use cases have a call()
method too.
injection_container.dart
//! Features - Number Trivia
//Bloc
sl.registerFactory(
() => NumberTriviaBloc(
concrete: sl(),
random: sl(),
inputConverter: sl(),
),
);
Stream
s of a Bloc) from the dispose()
method of a StatefulWidget
.Having a singleton for classes with this kind of a disposal would lead to trying to use a presentation logic holder (such as Bloc) with closed
Stream
s, instead of creating a new instance with opened Stream
s whenever you'd try to get an object of that type from GetIt
.Using type inference, the call to sl()
will determine which object it should pass as the given constructor argument. Of course, this is only possible when the type in question is also registered. It's apparent that we now need to register the GetConcreteNumberTrivia
and GetRandomNumberTrivia
use cases and also the InputConverter
. These will not be registered as factories, instead, they will be singletons.
Registering Singletons
GetIt
gives us two options when it comes to singletons. We can either registerSingleton
or registerLazySingleton
. The only difference between them is that a non-lazy singleton is always registered immediately after the app starts, while a lazy singleton is registered only when it's requested as a dependency for some other class.
In our case, choosing between lazy and regular registration doesn't make a difference, since the Number Trivia App will have only one page with one Bloc and one "dependency tree", meaning that even the lazy singletons will be registered immediately. We're going to opt in for registerLazySingleton
.
Bloc Dependencies
To keep registrations organized, everything goes under a special comment. Creating separate functions is also possible, but I feel like that can actually worsen the readability of the code if you have just a few registrations as we do.
injection_container.dart
//! Features - Number Trivia
...
// Use cases
sl.registerLazySingleton(() => GetConcreteNumberTrivia(sl()));
sl.registerLazySingleton(() => GetRandomNumberTrivia(sl()));
//! Core
sl.registerLazySingleton(() => InputConverter());
Repository Registration
While InputConverter
is a stand-alone class, both of the use cases require a NumberTriviaRepository
. Notice that they depend on the contract and not on the concrete implementation. However, we cannot instantiate a contract (which is an abstract class
). Instead, we have to instantiate the implementation of the repository. This is possible by specifying a type parameter on the registerLazySingleton
method.
injection_container.dart
//! Features - Number Trivia
...
// Repository
sl.registerLazySingleton<NumberTriviaRepository>(
() => NumberTriviaRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
),
);
NumberTriviaRepository
's underlying implementation for a different one without any changes to the dependent classes.Data Sources & NetworkInfo
Repository also depends on contracts, so we're again going to specify a type parameter manually.
injection_container.dart
//! Features - Number Trivia
...
// Data sources
sl.registerLazySingleton<NumberTriviaRemoteDataSource>(
() => NumberTriviaRemoteDataSourceImpl(client: sl()),
);
sl.registerLazySingleton<NumberTriviaLocalDataSource>(
() => NumberTriviaLocalDataSourceImpl(sharedPreferences: sl()),
);
//! Core
...
sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl(sl()));
External Dependencies
We've moved all the way down the call chain into the realm of 3rd party libraries. We need to register a http.Client
, DataConnectionChecker
and also SharedPreferences
. The last one is a little tricky.
Unlike all of the other classes, SharedPreferences
cannot be simply instantiated with a regular constructor call. Instead, we have to call SharedPreferences.getInstance()
which is an asynchronous method! You might think that we can simply do this:
sl.registerLazySingleton(() async => await SharedPreferences.getInstance());
The higher-order function, however, would in this case return a Future<SharedPreferences>
, which is not what we want. We want to register a simple instance of SharedPreferences
instead.
For that, we need to await
the call to getInstance()
outside of the registration. This will require us to change the init()
method signature
injection_container.dart
Future<void> init() async {
...
//! External
final sharedPreferences = await SharedPreferences.getInstance();
sl.registerLazySingleton(() => sharedPreferences);
sl.registerLazySingleton(() => http.Client());
sl.registerLazySingleton(() => DataConnectionChecker());
}
Initializing
The init()
method won't just get magically called by itself. It's our responsibility to invoke it and the best place for this service locator initialization is inside the main()
function.
main.dart
import 'injection_container.dart' as di;
void main() async {
await di.init();
runApp(MyApp());
}
await
the Future
even though it only contains void
. We definitely don't want the UI to be built up before any of the dependencies had a chance to be registered.Coming Up Next
Dependency injection was the missing link between the production code and test code, where we sort of injected the dependencies manually with mocked classes. Now that we've implemented the service locator, nothing can stop us from writing Flutter widgets which will utilize every bit of code we've written until now by showing a fully functional UI to the user.
I think there is a small issue in this approach.
In my understanding, unless your next set of lines doesn’t depend on objects returned from your await statement, those lines are simply executed. This would mean the runApp() is called as is without worrying about the await statement (which is what you don’t want!).
If I understand your saying, it’s not true because init returns Future and should be awaited anyway.
Check it in DartPad:
void main() async {
print(‘main first’);
await init();
print(‘main last’);
}
Future init() async {
final foo = await Future.delayed(
Duration(seconds: 2),
() => “init awaited”,
);
print(foo);
print(‘init next’);
}
Hey Matej …..Thank you for creating this marvelous series. Every Video in this series is helpful to write clean code.
I tried to incorporate get_it package in my application and it works beautifully in almost every scenario but it only fails during DI of BLOC instance.
One BLOC dependency on another BLOC doesn’t work using get_it. can you please help me to resolve this issue using get_it?
This maybe solves your problem: https://github.com/felangel/bloc/issues/711#issuecomment-569194920
If I have multiple dependency injection containers, where do I register them? All of them one after the other in the main or …?
Great series! Can you help me out? I am stuck with the registration of SharedPreferences. It seems that the await won’t work.
Seems like they changed the async initialisations. Still figuring out how to implement this with lazySingelton
https://github.com/fluttercommunity/get_it#synchronizing-asynchronous-initialisations-of-singletons
I think I found a soluton. Follow the guide in the link above. And change the registration as mentiont below. If anyone has a better solution, please tell me :)!
Change the registration of the SharedPreferences to: “sl.registerSingletonAsync(() async => SharedPreferences.getInstance());” and move it to the top of the file. After that you need to use “dependsOn: [SharedPreferences]” on the local data source.
Could you please explain how to use “dependsOn: ..” in the local data source. I am facing the same issue with the implementation of the sharedPreferences.
Oh, seems like I’ll have to update the code on GitHub again…
TDD series is absolutely Wooowww!!!
I too stuck with the same issue in SharedPreference dependency injection… 🙁
what if I have multiple implementations of repository and I want to switch between them at runtime?
+1
Great series. But how to apply this to stacked architecture with views and view models?
I want to use some values of shared preference in dio class and in some other blocs, what is the best way to do this, should i use shared preference directly by adding dependency in constructor or add local data source dependency and use shared preference there?