📜 ⬆️ ⬇️

Friendly introduction to Dagger 2. Part 2

Use modules to specify how objects should be created.


In the previous article in this series, we looked at how Dagger 2 saves us from the routine of writing initialization code by introducing dependencies.

If you remember, we created an interface that allows the framework to find out which class objects are required by our main method, and Dagger automatically generated a specific class that can initialize instances of these classes for us. We have not indicated anywhere exactly how to create these objects or their dependencies. Since all of our classes were specific and labeled with appropriate annotations, this did not create any problems: Dagger could make a conclusion from the annotations whose constructors are needed to create an instance of this class.

However, more often than not, classes depend not on concrete classes, but on abstract classes and interfaces that do not have constructors that Dagger could call. Sometimes changing the class source to include annotations is not an option at all. It also happens that creating an object requires more actions than just calling the constructor. In all these cases, Dagger doesn’t have enough automatic behavior, and the framework needs our help.
')
In today's article we will see how to provide Dagger with additional instructions on how to create objects using modules . Modules are interchangeable and can be used in other projects. Plus they can take arguments in runtime, which makes them even more flexible.

Example


In order to illustrate the situation described above, let's return to the first example from the previous article, where we only had 3 classes: WeatherReporter and 2 of its dependencies, LocationManager and WeatherService . Both dependencies were concrete classes. In practice, this may not happen.

Let's assume that WeatherService is an interface, and we have another class, say, YahooWeather , which implements this interface:

package com.example; public interface WeatherService { } 

 package com.example; import javax.inject.Inject; public class YahooWeather implements WeatherService { @Inject public YahooWeather() { } } 

If we again try to compile the project, Dagger will give an error and say that it cannot find a provider for WeatherService .

When a class is concrete and it has an annotated constructor, Dagger can automatically generate a provider for this class. However, since WeatherService is an interface, we need to provide Dagger with more information.

What are modules?


Modules are classes that can create instances of specific classes. For example, the following module is able to create WeatherService objects upon request, creating an instance of the YahooWeather class.

 @Module public class YahooWeatherModule { @Provides WeatherService provideWeatherService() { return new YahooWeather(); } } 

Modules must be annotated with @Module . Some of their methods, also known as provider-methods , are annotated with Provides to indicate that they can provide instances of a particular class upon request. Method names don't matter: Dagger looks only at signatures.

Using modules, we are improving Dagger's ability to create objects and resolve dependencies. Previously, only specific classes with annotated constructors could be used as dependencies, and now, with a module, any class can depend on the WeatherService interface. It only remains for us to connect this module to the component that is used at the entry point of our application:

 @Component(modules = {YahooWeatherModule.class}) interface AppComponent { WeatherReporter getWeatherReporter(); } 

The project is compiled again. Each WeatherReporter instance created by the getWeatherReporter method creates an instance of YahooWeather .

We can create one large module that knows how to create any abstract class or interface in an application, or many small modules that are involved in creating classes directly related to each other. In this series of articles we will use the second approach.

How to replace modules


One of the most interesting properties of the module is that it can easily be replaced by any other. This allows us to have multiple interface implementations in the project and easily switch between them.

Suppose we have another class, WeatherChannel , also implementing WeatherService . If we want to use this class in WeatherReporter instead of YahooWeather , we can write a new module WeatherChannelModule and substitute it in the component.

 @Module public class WeatherChannelModule { @Provides WeatherService provideWeatherService() { return new WeatherChannel(); } } 

 @Component(modules = {WeatherChannelModule.class}) public interface AppComponent { WeatherReporter getWeatherReporter(); } 

Replacing modules is useful, for example, when writing integration and functional tests. You can define another component, almost identical to the first, but using several other modules. We will return in more detail to this issue in the next article.

If we try to connect to the component two different modules that return the same type, Dagger will generate a compilation error, saying that the type is bound many times ( type is bound multiple times ). For example, this will not work:

 @Component(modules = {WeatherChannelModule.class, YahooWeatherModule.class}) public interface AppComponent { WeatherReporter getWeatherReporter(); } 

In addition, since Dagger 2 generates components during compilation, modules cannot be replaced in runtime. However, our entry point can have several components at its disposal and decide from which of them to receive objects based on specific conditions at run time.

Creating more complex objects


Now that we know how to write a simple module and what it is all about, let's start writing modules that create more complex objects. For example, instances of classes defined by a third party (which cannot be changed), having dependencies or requiring configuration.

Creating instances of third-party classes


Sometimes it is impossible to change the class and add annotation to it. For example, a class is part of a framework or a third-party library. Also quite often, third-party classes have a dubious design and do not facilitate the use of DI frameworks.

Suppose our LocationManager is dependent on GpsSensor . And this class is provided by the company "Horns and hoofs" and can not be changed. Let's complicate the situation even more: the class constructor initializes it not completely. After creating an instance of a class, before using it, we must also call methods such as calibrate . Below are the sources of LocationManager and GpsSensor .

 public class GpsSensor { public GpsSensor() { } public void calibrate() { } } 

 public class LocationManager { private final GpsSensor gps; @Inject public LocationManager(GpsSensor gps) { this.gps = gps; } } 

Please note that there are no annotations in GpsSensor . Since the products of a well-known company simply work , they do not need dependency injection or tests.

We would like to avoid calling the calibrate method in the LocationManager constructor, since setting up GpsSensor 'a is not its responsibility. Ideally, all accepted dependencies should already be ready to use . In addition, this instance of GpsSensor can be used in many places, and Horns and Hooves warn that a calibrate multiple call will result in crash.

To continue using GpsSensor without fear of consequences, we can write a module whose sole responsibility is to create and configure this object upon request. Thus, any class can depend on GpsSensor , without worrying about its initialization.

 @Module public class GpsSensorModule { @Provides GpsSensor provideGpsSensor() { GpsSensor gps = new GpsSensor(); gps.calibrate(); return gps; } } 

Creating objects with dependencies


Sometimes our modules need to create objects with dependencies. Creating these dependencies or searching for them is clearly not the responsibility of the module. Like any other class, he expects the dependencies to be provided to him in a finished form.

Suppose, for example, that the YahooWeather class requires WebSocket to work. Take a look at the code.

 public class WebSocket { @Inject public WebSocket() { } } 

 public class YahooWeather implements WeatherService { private final WebSocket socket; @Inject public YahooWeather(WebSocket socket) { this.socket = socket; } } 

Since the YahooWeather constructor now requires passing a parameter, we need to change the YahooWeatherModule . You must somehow get an instance of WebSocket to call the constructor.

Instead of creating dependencies right inside the module, which would nullify all the benefits of DI, we can simply change the provider signature. And Dagger already takes care of creating an instance of WebSocket .

 @Module public class YahooWeatherModule { @Provides WeatherService provideWeatherService(WebSocket socket) { return new YahooWeather(socket); } } 

Creating objects that require configuration


Sometimes a module needs to create an object that needs to be somehow configured, and this configuration requires information that is available only in runtime.

Let's imagine that the YahooWeather designer also needs an API key.

 public class YahooWeather implements WeatherService { private final WebSocket socket; private final String key; public YahooWeather(String key, WebSocket socket) { this.key = key; this.socket = socket; } } 

As you can see, we have removed the Inject annotation. Since our module is now responsible for creating YahooWeather , Dagger does not need to know about the constructor. Likewise, he does not know how to automatically inject the String parameter (although this is easy to do, as we will see in a future article).

If the API key is a constant available after compilation, for example, in the BuildConfig class, this solution is possible:

 @Module public class YahooWeatherModule { @Provides WeatherService provideWeatherService(WebSocket socket) { return new YahooWeather(BuildConfig.YAHOO_API_KEY, socket); } } 

Note, however, that the API key is available only in runtime. For example, it can be a command line argument. This information can be provided to the module, like any other dependency, through the constructor.

 @Module public class YahooWeatherModule { private final String key; public YahooWeatherModule(String key) { this.key = key; } @Provides WeatherService provideWeatherService(WebSocket socket) { return new YahooWeather(key, socket); } } 

This makes life a little more difficult for Dagger, since he no longer knows how to create such a module. When modules have constructors with no arguments, Dagger can easily initialize and use them. In our case, we must initialize our module ourselves and transfer it to Dagger for use. This can be done at the entry point to our application:

 public class Application { public static void main(String args[]) { String apiKey = args[0]; YahooWeatherModule yahoo = new YahooWeatherModule(apiKey); AppComponent component = DaggerAppComponent.builder() .yahooWeatherModule(yahoo) .build(); WeatherReporter reporter = component.getWeatherReporter(); reporter.report(); } } 

In lines 3-4, we get the API key from the command line arguments and create an instance of the module ourselves. In lines 5-7, we ask Dagger to create a new component using the newly created module instance. Instead of calling the create method, we call the builder 'y, pass in our module instance, and finally call build .

If there were more similar modules, we would have to initialize them in a similar way before calling build .

Conclusion


In today's article, we looked at how you can control the creation of objects through modules. Source codes for the article are available on Github .

In the next article, we will look at how to indicate that certain dependencies can be used by a set of objects.

Note from the translator. While the author has not written a sequel, I’m inviting all interested to continue their acquaintance with Dagger for a wonderful series of posts from xoxol_89 ( part 1 , part 2 )

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


All Articles