Introduction To Dependency Injection
Dependency Injection is a technique for resolving object dependencies during class initialization. It allows the creation of dependent objects outside of a class.
This technique allows us to write clean code where we can program against interfaces rather than concrete class types. It also enables to write testable codes.
For example, in the code below, class A requires an object of type B in it’s constructor.
class A {
B b;
A(B b) {
}
}
abstract class B {
doSomething();
}
class C implements B {
doSomething() {
...
}
class D implements B {
doSomething() {
...
}
}
}
The class B is an abstract class which acts as an interface. Class C could be it’s real implementation and D be it’s implementation for testing purpose.
Depending upon whether we are running unit tests or running applications, class A’s constructor should receive a different instance of C or D. If the app is under test mode, you want to pass an object of type D in the A’s constructor. Otherwise, you want to pass an object of type C.
A dependency injection technique allows to resolve such dependencies in an application during runtime.
Dependency Injection In Flutter
The steps to make use of the dependency manager in our Flutter app is going to be like this:
- Create a dependency manager.
- Add all the dependencies for the application in this manager.
- Initialize the dependency manager when the application starts.
- Use Inherited Widget to keep this manager in app’s state.
- Get access to the dependency manager object via the app’s state.
- Resolve dependencies during runtime via the injector object.
For the purpose of this post, we will make use of a dependency injection plugin called “flutter_simple_dependency_injection“
Setup Dependency Manager
Once you have added the package into your Flutter application, create a base class for dependency manager.
import 'package:flutter_simple_dependency_injection/injector.dart';
abstract class DependencyManagerBase {
Injector injector;
}
We are making this class abstract in order to be able to create separate types of dependency manager based on test, development or production environments.
Now, add a concrete implementation for this manager.
class DependencyManager implements DependencyManagerBase {
@override
Injector injector;
DependencyManager() {
injector = Injector.getInjector();
// Add your dependencies here
injector.map<B>((i) => C());
}
}
We have initialized Injector in the constructor of the DependencyManager. The Injector object holds the list of all dependencies in your application.
Now we need to make use of the dependency manager. For this purpose, we will use the Inherited Widget.
Using Inherited Widget For Dependency Injection
If you are completely new to Inherited Widget, you should check out Inherited Widget first.
class AppStateProvider extends InheritedWidget {
final AppState state;
final DependencyManagerBase diBase;
AppStateProvider(this.diBase, {Key key, Widget child})
: state = new AppState(diBase),
super(key: key, child: child){
state.init();
}
@override
bool updateShouldNotify(_) => false;
static AppState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(AppStateProvider)
as AppStateProvider)
.state;
}
}
Here the AppStateProvider class is an InheritedWidget which manages the app state. The dependency manager is passed onto the AppState class.
Once you the InheritedWidget setup, you can initialize the dependency manager like this:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppStateProvider(
DependencyManager(),//initialize the dependency manager
child: MyHomePage(),
);
}
}
This allows us to get the AppState anywhere in the application by calling the static method “of“.
final appState = AppStateProvider.of(context);
Finally, you can access the injector object from anywhere in the application via the AppState object.
var diManager = widget.appState.diBase;
var a = A(diManager.injector.get<B>());