📜 ⬆️ ⬇️

Dagger 2. Subcomponents. Best practice

On Habré, there have already been several good articles on installing and working with Dagger 2 :


I want to share my experience using Dagger 2 on real projects with real cases. Reveal to the reader the power and convenience of both Dagger himself and such an aspect as Subcomponent.

Before you go under the cat, you should read the above articles.
Anyone interested, you are welcome!
image

A friend of mine taught me a great way how to sort everything out: representing some architecture (either a single class, or even a small piece of code), try to transfer it to the real world. Find in your everyday life something similar to the logic of your code. And then, based on the example of real life, you will understand how this or that program component (object) should behave. Understand what the result should end up.
')
This time I will do the same.

Let's digress from programming and fast forward to the surgery room.

Saving lives is an extremely important task. Each member of the medical team must accurately carry out their work and not prevent others from doing their own.

On the shelves neatly laid out all the tools. The head physician with great concentration and painstakingly performs the operation, occasionally turning to an assistant to get a new instrument, say, a scalpel. At different points in time, it may be necessary to have a “different scalpel”, and therefore it is important for the assistant to also not be distracted from the process and deliver exactly the right tool.

The doctor absolutely does not care about the shelf on which the instrument he needs is lying. It is more important for him to concentrate fully on the operation in order to avoid mistakes - this is his area of ​​responsibility .

The assistant is responsible for the availability of all the necessary tools for the operation, for their cleanliness, for the timely provision of tools to the doctor. Well and the most interesting, the assistant decides on his own, depending on the situation, which tool to choose; Of course, if the doctor did not have precise instructions.

In our case, the assistant is Dagger. The doctor is our program component with a clear purpose in the program. It is in the delegation (from the doctor to the assistant) of the creation and provision of dependencies (tools) that the pattern is - Dependency Injection (dependency injection).

What can be learned from this example:

  1. The component should not contain the logic of creating other components.

  2. The component should not care about the implementation of its tools. In our example, if the surgeon asks: “A scalpel!”, The situation assistant will return the desired one from the set. So we can say that the doctor does not work with specific implementations of tools, but with their interfaces.

Practice. Let's go back to programming.

In the vast majority of applications there is work with any list of data.
The adapter, which accepts to the constructor, is responsible for data processing:

  1. Listener - for interaction events with list items;
  2. Perhaps context or LayoutInflater to create ViewHodlers;
  3. Well, the data list itself, if, of course, it was initialized in advance (otherwise the adapter implements its setList () method).

But what is the result? Having received in our Fragment'e (or Activity) design

Adapter adapter = new Adapter(this, getContext(), list); recyclerView.setAdapter(adapter); 

We preoccupied our component by initializing another component. Our doctor moved away from the operating table to find the right instrument.

image


With Dagger, however, we will not just get rid of the first line of the presented code, namely, we will free the component from the logic of creating another component - from unnecessary logic for it.

Wait a minute, a question may appear here:

If the adapter initialization is delegated to Dagger, where does it get the Listener (the object of our component that implements the Listener)? Storing a singleton fragment or activation is more than a bad idea!

Such a question may arise if you:

  1. Use one or two Components for the entire application;
  2. All dependencies store singletonami;
  3. And you don't want to know about Subcomponents and Component dependency.

Let's go into a small abstraction, which I lacked at the beginning of studying Dagger.

Most examples of using Dagger in the “Internet” necessarily include creating the so-called AppComponent with its AppModule with root Context dependency (or your class extending Application, which is essentially also Context).

We will understand why.

"In the beginning was the word ..."

Having Context, we can get other dependencies, for example: SharedPreferences, LayoutInflater, some system service, etc. Accordingly, having SharedPreferences, we can receive PreferenceHelper - a class-utility for working with preferences. Having LayoutInflater, we can receive any ViewFactory. From these “higher level” dependencies, we can also get more and more complex, complex ones. And all this diversity has gone from only one object - the context. In this case, it can be called the core of our AppComponent.

image

And all of the above is just those dependencies that must exist throughout the life of the entire application, i.e. Singleton'y. That is why as the core we have the object that exists all this time - the object context of the application.

Continuing this thought, let us think how long should our Adapter exist? Obviously, while there is a screen with which this adapter works.

Adapter, we will provide a ViewHolderFactory, which must exist as long as the Adapter exists. In addition to the Adapter, we will give Fragment some ViewController, and it should also exist, only as long as the Fragment exists, etc.

If to understand, all dependences used exclusively while this screen is “alive” depend on this screen. So we can say that our Fragment (or Activity) will be the core of our local Component, the Component, which exists while our screen exists.

image

To realize a well-defined lifetime of this local handful (column) of our dependencies, we will use Subcomponent.

Ask me "how?"

For now, let's forget about the sub prefix and imagine that we implement just the Component. If it will be easier for you, imagine that our screen is the whole of our application.

To begin with, we need a base module. Since our list screen, call it ListModule.

 @Module public class ListModule { } 

Now we need the very core - the basic dependence, from which all the rest will go. As mentioned earlier, the basic dependency for the screen is the "screen object" itself - for example, ListFragment. Let's transfer it in the designer of the module.

 @Module public class ListModule { private final ListFragment fragment; public ListModule(ListFragment fragment) { this.fragment = fragment; } } 

The basis is, further creativity.

Provide our adapter:

 @Provides public Adapter provideAdapter(Context context) { return new Adapter(fragment, context, fragment.initList()); } 

NOTE: We have a Context, but obviously we did not provide it in this module or in other modules of our Component. More on this later.

You can even separately provide the list of data itself (this is redundant, but for example it will do):

 @Provides public List<Model> provideListOfModels() { return fragment.initList(); } @Provides public Adapter provideAdapter(Context context, List<Model> list) { return new Adapter(fragment, context, list); } 

Now, to make it work as it should, a few settings.

In order to prompt Dagger, that:

  1. All dependencies of Component are one graph, separate from the main one;
  2. We want not to create a new dependency every time, but to cache the only one;

There are so-called Scope annotations. Each Scope annotation looks like this:

 @Scope @Retention(RetentionPolicy.Runtime) public @interface Singleton {} 

A singleton is a basic annotation provided by Dagger. It is provided simply so that you have something to push off from. The “singleton” itself will not happen in a magical way, unless you save your AppComponent in the App class (the class extending Application). Those. Dagger guarantees that a single dependency instance will be created for a given instance of Component. But you are responsible for the uniqueness of the Component instance.

Similarly, create your scope-annotation:

 @Scope @Retention(RetentionPolicy.Runtime) public @interface ListScope {} 

Our annotation does not give way to the annotations of Singleton, the whole point is how we use them.

Scope-annotation we mark our provide-methods and Component, containing our modules.

IMPORTANT: In one Component signed by a certain Scope there can only be modules whose provide methods are signed by the same Scope. So we do not intersect two different dependency graphs.

The final view of our ListModule:

 @Module public class ListModule { private final ListFragment fragment; public ListModule(ListFragment fragment) { this.fragment = fragment; } @ListScope @Provides public List<Model> provideListOfModels() { return fragment.initList(); } @ListScope @Provides public Adapter provideAdapter(Context contex, List<Model> list) { return new Adapter(fragment, context, list); } } 

And our Component:

 @ListScope @Subcomponent(modules = ListModule.class) public interface ListComponent { void inject(ListFragment fragment); } 

The key here is the @Subcomponent annotation. So we inform you that we want to have access to all dependencies of our parent Component, but note that we don’t indicate the parent here. In our example, the parent will be AppComponent.

* It is from AppComponent that we get the Context for initializing the adapter.

To get your Subcomponent, in the parent Component you need to describe the method of getting it, passing all the Subcomponent modules to the arguments (in our case only one module).

What it looks like:

 @Singleton @Component(modules = AppModule.class) public interface AppComponent { ListComponent listComponent(ListModule listModule); } 

Dagger will take care of the implementation of this method.

We organize the time of life

As already mentioned, AppComponent is because Singleton, because we store its only instance in the App class. We can create an instance of our Subcomponent only with the help of the parent, and therefore all the logic of receiving and storing the Subcomponent can also be transferred to the App class, with one important difference: We will add the ability to create a Subcomponent at any time and destroy it at any time.

In the App class, we describe the following logic:

 public class App extends Application { private ListComponent listComponent; public ListComponent initListComponent(ListFragment fragment) { listComponent = appComponent.listComponent(new ListModule(fragment)); return listComponent } public ListComponent getListComponent() { return listComponent; } public void destroyListComponent() { listComponent = null; } } 

NOTE: On large projects, it makes sense to bring the logic of working with Dagger from the App class to the helper class using composition.

Well, it remains to describe the use of all this in our fragment:

 public class ListFragment extends Fragment { @Inject Adapter adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); App.getInstance().initListComponent(this).inject(this); init(); } private void init() { recyclerView.setAdapter(adapter); } @Override public void onDestroy() { super.onDestroy(); App.getInstance.destroyListComponent(); } } 

Thus, we tied the life time of our graph to the life cycle of a fragment.

This may seem redundant in the case of one dependency (although, even with one dependency, taking out such logic makes your code cleaner and less hooked ). Most of the work is architecture. And so now, if you need to provide a new dependency, it will be reduced to the implementation of one provide-method.
image

BONUS


When all dependencies are highlighted in provide-methods, there appears such a pleasant bun, like getting rid of any dependencies. Consider again the example with the adapter.

ListFragment implements Listener of the events connected with ViewHolder objects of our list. Accordingly, in order to deliver the Listener to each ViewHolder, it becomes necessary to store a reference to the Listener in the Adapter.

Get rid of the middleman.

image


A good practice is to take away the creation of ViewHolders in the ViewHolderFactory. So do:

 public class ListItemViewHolderFactory { private final Listener listener; private final LayoutInflater layoutInflater; public ListItemViewHolderFactory(LayoutInflater layoutInflater, Listener listener) { this.layoutInflater = layoutInflater; this.listener = listener; } public ListItemViewHolder createViewHolder(ViewGroup parent) { View view = layoutInflater.inflate(R.layout.item, parent, false); return new ListItemViewHolder(view, listener); } } 

Our module will change to this type:

 @Module public class ListModule { private final ListFragment fragment; public ListModule(ListFragment fragment) { this.fragment = fragment; } @ListScope @Provides public List<Model> provideListOfModels() { return fragment.initList(); } @ListScope @Provides public Adapter provideAdapter(ListItemViewHolderFactory factory, Context context, List<Model> list) { return new Adapter(factory, context, list); } @ListScope @Provides public ListItemViewHolderFactory provideVhFactory(LayoutInflater layoutInflater) { return new ListItemViewHolderFactory (layoutInflater, fragment); } } 

NOTE: Remember to provide the LayoutInflater in the AppModule.

It seems to me that this example shows well how flexible the work with dependencies becomes.

Now imagine the world in which we make the code review of a certain component (class), and we see only its logic. There is no need to "jump" between software components to track the thread of events. External tools appear by themselves, and with other components ours interact through interfaces (or do not interact at all).

image

I hope this article has given you a ground for reflection and creativity, and the world of Dagger has become a little closer.

The next time we analyze the second part of the assistant’s functionality, return a specific implementation of the scalpel depending on the situation. Let's talk about the authorized zone and work with social networks.

Thanks for attention.

The example described in the article on githaba.

UPD: The second part

Source: https://habr.com/ru/post/334710/


All Articles