Hello!
We continue our series of articles on Dagger 2. If you have not yet read the first part , do it immediately :)
Many thanks for the feedback and comments on the first part.
In this article we will talk about custom scopes, about linking components through component dependencies and subcomponents. And also we will touch upon such an important question as the architecture of the mobile application, and how Dagger 2 helps us build a more correct, module-independent architecture.
I ask all those interested under the cat!
Let's start with architecture. Recently, a lot of attention has been paid to this issue, many articles and speeches are devoted. The question is certainly important, because from the way we call the boat, so it will float. Therefore, I highly recommend to start reading these articles:
I really like the Clean Architecture approach in building architecture. It allows for a clear vertical and horizontal construction of all modules, where each class does only what it has to do. For example, Fragment is responsible only for displaying the UI, and not implementing requests to the network, database, implementation of business logic and other things, which made Fragment just a huge piece of tangled code. I think many people know this ..
Consider an example. There is an application. There are several modules in the application, one of which is a chat module. The chat module includes three screens: a single chat screen, a group chat and settings.
Remembering Clean architecture, we distinguish three horizontal levels:
Context
(global context), RxUtilsAbs
(utility class), NetworkUtils
(utility class), and IDataRepository
(class responsible for server requests).IChatInteractor
(a class that implements specific Chat business cases) and IChatStateController
(a class that is responsible for Chat status).Schematically, life cycles will look like this:
Remember, in the last article we mentioned the "local" singltons? So, the objects of the chat levels and each chat screen are "local singletons", that is, objects whose life cycle is longer than the life cycle of the standard activation / fragment, but less than the life cycle of the entire application.
But now Dagger 2 comes into play, which has a wonderful Scopes mechanism. This mechanism assumes the creation and storage of a single instance of the required class as long as the corresponding scope exists. I am sure that the phrase "while the existing scope exists" is somewhat confusing and raises questions. Do not worry, everything will become clear below.
In the last article, we tagged the "global singletons" scope @Singleton
. This scope has existed for the entire lifetime of the application. But we can also create our own custom scope annotations. For example:
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ChatScope { }
And creating the @Singleton
annotation in Dagger 2 looks like this:
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface Singleton { }
That is, @Singleton
is no different from @ChatScope
, just the @Singleton
annotation @Singleton
provided by the default library. And the purpose of these annotations is one thing - specify Dagger, provide "scope" or "unscoped" objects. But, again, I repeat, we are responsible for the life cycle of the "scope" of objects.
We return to our example. According to the current architecture, we get three groups of objects that have their own "length of life". Thus, we need three scope annotations:
@Singleton
- for global singltons.@ChatScope
- for Chat objects.@ChatScreenScope
- for objects of a particular Chat screen.In this case, we note that @ChatScope
objects must have access to @Singleton
objects, and @ChatScreenScope
- to @Singleton
and @ChatScope
objects.
Schematically:
Next comes the creation and the corresponding component of Dagger:
AppComponent
that provides "global singletones".ChatComponent
providing "local singltons" for all Chat screens.SCComponent
, which provides "local singltons" for a specific Chat screen ( SingleChatFragment
, i.e., Single Chat screen).And again we visualize the above:
As a result, we obtain three components with three different scope-annotations that are connected to each other in a chain. ChatComponent
depends on AppComponent
, and SCComponent
on ChatComponent
.
But now the question arises, how do we properly link these components? There are two ways.
This communication method was pumping from Dagger 1.
We note immediately the features of Component dependencies:
As in our example, the dependency diagram will look like with component dependencies:
Now we will consider each component with its modules separately.
Appcomponent
Note that in the interface of the component we explicitly specify the objects that will be available for the child components ( but not for the subsidiaries of the child components , let's talk about this situation a bit later). For example, if the child component wants NetworkUtils
, then Dagger will give the corresponding error.
In the interface, we can also still set injection targets. That is, you should not be mistaken that if a component has child components, then it cannot inject its dependencies into the necessary classes (activations / fragments / other).
Chatcomponent
In the annotation for ChatComponent
we explicitly prescribe what component ChatComponent
should depend ChatComponent
(depends on AppComponent
). Yes, as it was noted earlier, the component component may have several parents (it is enough just to add new parent components to the annotation). But the scope annotations of the components should be different. And also in the interface we explicitly register those objects to which the child components can have access.
Notice the green arrow? As we have said, ChatComponent
can use dependencies on AppComponent
, which it explicitly indicated. But the ChatComponent
child components ChatComponent
no longer use AppComponent
, unless we explicitly register these dependencies in ChatComponent
, which we actually did for Context
.
SccomponentSCComponent
is SCComponent
dependent, and it SingleChatFragment
dependencies into a SingleChatFragment
. In this case, in SingleChatFragment
this component can inject both SCPresenter
and other objects of the parent components, which are explicitly specified in the corresponding interfaces.
Stayed last step. This is to initialize the components:
Compared to a regular component, when the dependent component is initialized in the DaggerChatComponent and DaggerSCComponent
, another method appears - appComponent(...)
(for DaggerChatComponent
) and chatComponent(...)
(for DaggerSCComponent
), into which we specify initialized parent components.
By the way, if a component has two parents, then two corresponding methods appear in the builder. If there are three parents, then there are three methods, etc.
Since all components have their own life cycle, different from the activation / fragment life cycle, we will initialize and store the component instances in the Application file. An example of the Application class is considered at the end.
The feature is already Dagger2.
Features:
Yes, Subcomponents have some differences from Component dependencies. Consider the schema and code to better understand the differences.
According to the scheme, we see that all objects of the parent are available for the child component, and so on throughout the component dependency tree. For example, SCComponent
is available for NetworkUtils
.
Appcomponent
The next difference is Subcomponents. In the AppComponent
interface, AppComponent
create a method for the subsequent initialization of ChatComponent
. Again, the main thing in this method is the return value ( ChatComponent
) and the arguments ( ChatModule
).
I note that ChatModule
don’t need to pass anything to the constructor of our ChatModule
(the default constructor), so you can omit this argument in the plusChatComponent
method. However, for a clearer picture of dependencies and for educational purposes, for the time being we will leave everything as detailed as possible.
ChatcomponentChatComponent
- is both a child and a parent component. SCComponent
parent is indicated by the method of creating an SCComponent
in the interface. And the fact that a component is a child is indicated by the @Subcomponent
annotation.
Sccomponent
As we noted earlier, since all components have their own life cycle, which is different from the activation / fragment life cycle, we will initialize and store the instances of the components in the Application file:
public class MyApp extends Application { protected static MyApp instance; public static MyApp get() { return instance; } // Dagger 2 components private AppComponent appComponent; private ChatComponent chatComponent; private SCComponent scComponent; @Override public void onCreate() { super.onCreate(); instance = this; // init AppComponent on start of the Application appComponent = DaggerAppComponent.builder() .appModule(new AppModule(instance)) .build(); } public ChatComponent plusChatComponent() { // always get only one instance if (chatComponent == null) { // start lifecycle of chatComponent chatComponent = appComponent.plusChatComponent(new ChatModule()); } return chatComponent; } public void clearChatComponent() { // end lifecycle of chatComponent chatComponent = null; } public SCComponent plusSCComponent() { // always get only one instance if (scComponent == null) { // start lifecycle of scComponent scComponent = chatComponent.plusSComponent(new SCModule()); } return scComponent; } public void clearSCComponent() { // end lifecycle of scComponent scComponent = null; } }
And now we can finally see the life cycle of the components in the code. Everything is clear about AppComponent
, we initialized it when the application started and we don’t touch it anymore. But we initialize ChatComponent
and SCComponent
as needed using the plusChatComponent()
and plusSCComponent
. These methods are also responsible for returning single instances of the component.
So when you call again, for example,scComponent = chatComponent.plusSComponent(new SCModule());
a new instance of SCComponent
with its dependency graph.
With the help of the clearChatComponent()
and clearSCComponent()
methods, we can end the life of the corresponding components with their graphs. Yes, the usual reference links. If ChatComponent
and SCComponent
needed again, then we simply call the plusChatComponent()
and plusSCComponent
that create new instances.
Just in case, I’ll clarify that in this example we cannot initialize ChatComponent
when SCComponent
is not initialized ChatComponent
we will snatch a NullPointerException
.
Also note that if you have a lot of components and subcomponents, then it is better to put all this code from MyApp
into a special singleton (for example, Injector
), which will be responsible for creating, destroying and providing the necessary Dugger components.
That's all. As you have seen, custom scopes, component dependencies and subcomponent are extremely important elements of Dagger 2, with which the developer can create a more structured and correct architecture.
In addition to reading I recommend the following articles:
I will be glad to your comments, comments, questions and likes :)
In the next article we will consider the use of Dagger 2 in testing, as well as additional, but no less important and functional features of the library.
Source: https://habr.com/ru/post/279641/
All Articles