Dagger 2 and the structure of the application for Android
Good afternoon! Our team has been developing the MyOffice email client for the Android platform for more than a year (we are developing MyOffice applications for all popular platforms).
Today we want to talk about the technologies that we use in the development of our email client. Namely, about the mechanisms of Dependency Injection in the form of the Dagger 2 library. In the article we will describe the main parts of the library and tell how to use them in an Android project.
Why Dagger 2
Before we started using Dagger 2, we did not use the Dependency Injection (DI) pattern. This is like adding too much cereal to the mess: our code was too connected and this interfered with free testing and editing of the code.
At the same time, Google announced the Dagger 2 library - it was the newest option. We compared the available analogs and for the mail client MyOffice we focused on it.
The Dagger 2 library has several advantages over other Dependency Injection libraries. Its fundamental advantage is the work on the principle of code generation, without reflection. It follows that any errors associated with building a dependency graph will be discovered even at the time of compilation of the project.
By implementing DI in our project, we were able to beautifully get rid of the strong connectivity between the various modules of our application. We were also able to remove most of the singletones, the use of which was unjustified. Already now we see how the efficiency of writing and editing code has improved. In the future, we will have the opportunity to simplify the task of covering the Unit and UI project with tests, which in turn will increase the stability of the application.
In this article, we want to provide a complete overview of Dagger 2.
We will cover the main parts of Dagger 2:
dependency request options;
modules providing objects for implementation;
components connecting requests with objects for implementation;
and tell you how to use additional parts of Dagger 2:
deferred and asynchronous dependency initialization.
Views @ Inject
There are several ways to request a dependency:
1) Embedding in a class constructor. A bonus of this option is the implicit availability of using this dependency for implementation (ManagerA is not required to be specified in the module). If the constructor has parameters, it is necessary that they are in the dependency graph and can be implemented.
//тут можно поместить @Scope зависимости
public class ManagerA{
@Inject
public ManagerA(Context context){ /* */}
}
2) Implementation through the method. The method will be executed after calling the constructor.
@Inject
public void register(SomeDepends depends){
depends.register(this);
}
3) Implementation in the class fields. Fields must not be private or final.
@Inject ManagerB managerB;
4) The getter call of the object we need. This getter is also used to link multiple dependency graphs.
managerA = component.getManagerA();
@ Module
A module is a factory of objects that resolves our dependencies. It should be tagged with @ Module annotation, and dependency generating methods should be @ Provides. And if it is necessary to mark the scope, then we mark the module with one of the @ Scope annotations.
Thus, the dependencies contained in them will be available in the module that refers to them.
A module may contain a constructor with a parameter if it needs external data to resolve dependencies. The presence of a constructor makes an important difference in the creation of a component, which will be discussed below .
A component is the link between modules and dependency seekers. You can give the dependency through the method of the component (to which the object requesting the dependencies will be passed) or through the getter (which will return the dependency). One component can have both methods and getters. The names of the methods or getters are not important.
In both cases, we first create the interface and mark it with the annotation @ Component or @ Subcomponent. Next, we specify how dependencies will be resolved. You must add a list of modules that will generate dependencies.
In case of implementation through the method, the list of necessary dependencies is taken from the class itself and its base classes:
class App{
@Inject
ManagerA managerA;
}
A component containing both the method and the getter will look like this:
Next you need to assemble the project. Classes of the form DaggerName of your Component will be generated that are the descendants of your component. To create an instance of the component, use the builder. Depending on whether the module has a constructor with parameters or not, we can act in different ways.
If there is a parameterized module constructor, then you need to set all such modules yourself:
AppModule module = new AppModule(this);
DaggerAppComponent.builder().appModule(module).build();
//сгенерированный код
public SecondActComponent build() {
if (appModule == null) {
throw new IllegalStateException("appModule must be set");
}
return new DaggerAppComponent (this);
}
If not, then in addition to the builder, the create () method will be generated and the build () method will be changed:
DaggerAppComponent.create();
//сгенерированный код
public static AppComponent create() {
return builder().build();
}
//сгенерированный код
public AppComponent build() {
if (appModule == null) {
this.appModule = new appModule();
}
return new DaggerAppComponent (this);
}
class App{
@Inject
ManagerA managerA;
AppComponent component
@Override
public void onCreate(){
//… инициализация компонента
component.inject(this);
//или
managerA= component.getmanagerA();
super.onCreate()
}
}
@ Scope
Consider Android and the use of scopes. The @ Scope annotation and its descendants mark the methods in the modules that generate the objects to embed. If the Produce method is marked with a scope, then any component using this module must be marked with the same scope.
Different managers have different scopes. For example, DataBaseHelper should be one for the entire application. For this, a singleton was usually used. In Dagger there is such scope @ Singletone, which marks the objects needed in one instance for the entire application. But we decided to use our @ PerApplication scopes for a complete analogy of names with activity and fragment scopes.
The name of the scope does not matter - the level of nesting of the components and their scopes is important.
Application level
Annotations that define scope are declared as follows:
class AppModule{
//...
@Provides
@PerApplication
DbHelper provideDbHelper(Context context){
return new DbHelper(context);
}
@Provides
@PerApplication
Context provideContext(){
return app;
}
}
Within the framework of one module and those that are registered with it in includes, the same scope must be used, otherwise during compilation you will get an error in building the dependency graph.
Now we must mark the components using this module:
It is worth paying attention to the fact that DI is convenient to use for tests, and we would like to be able to replace db with its imitation. To do this, it is advisable to put DbHelper in a separate module:
@Module
class DBModule{
@PerApp
@Provides
DbHelper dbhelper(Context context){
return new DbHelper(context);}
}
As you can see, this module does not contain context and is not able to independently resolve it. The context is taken from the module that refers to it:
There can be several Activity objects in an application, and their components need to be associated with the Application component. Consider the annotation parameters @ Component and @ Subcomponent and their participation in the construction of the dependency graph.
Suppose we have an EventBus manager for communication between an Activity and a fragment. Its scope is one instance of the manager for the Activity and fragments that are in the Activity.
@Module
class ActModule{
@PerActivity
@Provides
provide Bus(){return new Bus();}
@Component()
interface ActComponent{
inject(MainActivity activity);
}
class MainActivity extends Activity{
@Inject DbHelper dbHelper;
@Inject Bus bus;
}
But at compile time, they immediately tell us that ActComponent cannot inject the DbHelper dependency. Magic, of course, did not happen. We got two different unrelated dependency graphs. And the second graph does not know where to get DbHelper.
We have two options: either connect the components through an interface that will provide us with all the necessary dependencies, or, using the first component, create the second, then we get one graph.
The @Component annotation has a dependencies parameter that points to a list of component interfaces that provides the necessary dependencies.
For the second method, you need to mark our internal component with @ Subcomponent annotation. In addition to the list of modules, it has no other parameters.
And in AppComponent we add the method returning ActComponent. There is a general rule that if Subcomponent has a module with a parameterized constructor, it must be passed to our method. Otherwise, an error will occur at the time the component is created.
The disadvantage of the SubComponent option is that if ActComponent or ActModule contain several other modules, then you will need to increase the number of Plus method parameters in order to be able to transfer the changed module:
Total: the option with the component and dependencies looks more flexible, but it will be necessary to describe all the necessary dependencies in the interface.
Fragment Level
Embedding dependencies in fragments is more interesting, since a fragment can be used in several Activities. For example, an application with a list of objects and their detailed descriptions, when two Activities are used on the phone, and one Activity with two fragments on the tablet.
For our email client, we decided to use our own component for each fragment, even if you need to implement only one dependency. This will facilitate our work if you need to update the list of dependencies in the fragment. There are also two options for creating a component:
We immediately see the problem: our component depends on the specific component of Activity. A suitable solution is when an interface is created for each component of the fragment that describes the dependencies necessary for it:
Let's say we have a manager that takes a long time to initialize. I would not want all of these dependencies to occupy the main thread at once when starting the application. It is necessary to postpone the implementation of these dependencies until they are used. To do this, Dagger 2 has the Lazy and Provider interfaces that implement deferred dependency initialization.
@Inject
Lazy managerA;
@Inject
Provider managerA;
If ManagerA has some kind of scope, then their behavior is identical, but if there is no scope, Lazy caches the dependency after initialization, and Provider generates a new one each time.
Development of asynchronous dependency initialization is also underway. In order to look at them, you need to add:
compile 'com.google.dagger: dagger-producers: 2.0-beta'
In the following articles we are ready to talk about our mobile developments and the technologies used. Thank you for your attention and Happy New Year!