📜 ⬆️ ⬇️

Meet Dependency Injection with the Dagger example

http://radiant--eclipse.deviantart.com/
In this article we will try to deal with Dependency Injection in Android (and not only) using the example of the increasingly popular open source library Dagger
So what is Dependency Injection? According to Wikipedia, this is a design pattern that allows you to dynamically describe dependencies in code, dividing business logic into smaller blocks. This is convenient, first of all, by the fact that later these same blocks can be replaced by test ones, thereby limiting the testing area.

Despite the sophisticated definition, the principle is quite simple and trivial. I am sure that most of the programmers somehow implemented this pattern, even sometimes without realizing it.

Consider a simplified (before pseudocode) version of the Twitter client.

In theory, the dependency diagram looks like this:

')
Let's take a look at how this looks in code:

public class Tweeter { public void tweet(String tweet) { TwitterApi api = new TwitterApi(); api.postTweet("Test User", tweet); } } public class TwitterApi { public void postTweet(String user, String tweet) { HttpClient client = new OkHttpClient(); HttpUrlConnection connection = client.open("...."); /* post tweet */ } } 


As you can see, the set of interfaces is quite simple, so we will use it like this:

 Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world!"); 


As long as everything goes well, tweets fly away - everyone is happy. Now there is a need to test it all. Immediately, we note that it would be nice to be able to replace the Http client with the test one in order to return some test result and not break into the network every time. In this case, we need to remove the obligation to create the Http client from the TwitterApi class and download this duty to the higher classes. Our code is slightly transformed:

 public class Tweeter { private TwitterApi api; public Tweeter(HttpClient httpClient) { this.api = new TwitterApi(httpClient); } public void tweet(String tweet) { api.postTweet("Test User", tweet); } } public class TwitterApi { private HttpClient client; public TwitterApi(HttpClient client) { this.client = client; } public void postTweet(String user, String tweet) { HttpUrlConnection connection = client.open("...."); /* post tweet */ } } 


Now we see that, if necessary, to test our code, we can easily “substitute” the test Http client, which will return test results:
 Tweeter tweeter = new Tweeter(new MockHttpClient); tweeter.tweet("Hello world!"); 


It would seem, what could be easier? In fact, now we have “manually” implemented the Dependency Injection pattern. But there is one "but." Imagine a situation that we have a class Timeline, which can download the last n messages. This class also uses TwitterApi:



 Timeline timeline = new Timeline(new OkHttpClient(), "Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) { System.out.println(tweet); } 


Our class looks like this:

 public class Timeline { String user; TweeterApi api; public Timeline(HttpClient httpClient, String user) { this.user = user; this.api = new TweeterApi(httpClient); } public void loadMore(int n){/*.....*/} public List<Tweet> get(){/*.......*/} } 


It seems that everything is nothing - we used the same approach as with the Tweeter class - provided an opportunity to specify the Http client when creating the object, which allows us to test this module without depending on the network. But! Have you noticed how much code we have duplicated and how do we have to “drag” the Http client right from the application’s head? Of course, you can add default constructors that will create a real Http client, and use the custom constructor only when testing, but this does not solve the problem, but only masks it.

Let's look at how we can improve the situation.

Dagger



Dagger is an open source Dependency Injection library from the developers of okhttp, retrofit, picasso, and many other great libraries known to many Android developers.

The main advantages of Dagger (compared to the same Guice):


In Dagger, the dependency configuration process is divided into 3 large blocks:


Request dependencies


To ask Dagger to initialize one of the fields, all you need to do is add the @Inject annotation:

 @Inject private HttpClient client; 


... and make sure that this class is added to the dependency graph (more on that later)

Dependency satisfaction (provide dependency)


To tell Dagger what client instance you need to create, you need to create a “module” - annotated class @Module :

 @Module public class NetworkModule{...} 


This class is responsible for “satisfying” some of the dependencies requested by the application. In this class, you need to create a so-called "provider" - a method that returns the HttpClient instance (annotated @Provide ):

 @Module(injects=TwitterApi.class) public class NetworkModule { @Provides @Singleton HttpClient provideHttpClient() { return new OkHttpClient(); } } 


So we told Dagger to create an OkHttpClient for anyone who asked for HttpClient using @Inject annotation

It is worth mentioning that in order for compile-time validation to work, you must specify all the classes (in the injects parameter) that request this dependency. In our case, HttpClient is only needed for TwitterApi class.
The @Singleton tells Dagger to create only 1 client instance and cache it.

Creating a graph


We now turn to the creation of the graph. For this, I created a class Injector , which initializes the graph with one (or more) module. In the context of Android applications, it is most convenient to do this when creating an application (inherit from Application and overload onCreate() ). In this example, I created a TweeterApp class that contains the other components (Tweeter and Timeline)

 public class Injector { public static ObjectGraph graph; public static void init(Object... modules) { graph = ObjectGraph.create(modules); } public static void inject(Object target) { graph.inject(target); } } public class TweeterApp { public static void main(String... args) { Injector.init(new NetworkModule()); Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world"); Timeline timeline = new Timeline("Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) { System.out.println(tweet); } } } 


Now back to the dependency query:

 public class TwitterApi { @Inject private HttpClient client; public TwitterApi() { //     Injector.inject(this); //   ""  client  Dagger' } public void postTweet(String user, String tweet) { HttpUrlConnection connection = client.open("...."); /* post tweet */ } } 


Notice Injector.inject(Object) . This is necessary in order to add a class to the dependency graph. Those. if we have at least one @Inject in the class - we need to add this class to the graph. As a result, in our graph there should be all classes that request dependencies (each of these classes should make ObjectGraph.inject() ) + modules that satisfy these dependencies (usually added at the stage of graph initialization).

Now back to our original task - to test everything. We need to somehow be able to replace HttStack. The module NetworkModule is responsible for satisfying this dependency (hmm - just now noticed how interesting it sounds):
 @Provides @Singleton HttpClient provideHttpClient() { return new OkHttpClient(); } 


One option is to add some configuration file that will dictate which environment to use:
 @Provides @Singleton HttpClient provideHttpClient() { if(Config.isDebugMode()) { return new MockHttpClient(); } return new OkHttpClient(); } 


But there is an even more elegant option. In Dagger, you can create modules that override functions that provide dependencies. To do this, add the parameter overrides=true to the module:
 @Module(overrides=true, injects=TwitterApi.class) public class MockNetworkModule { @Provides @Singleton HttpClient provideHttpClient() { return new MockHttpClient(); } } 


All that remains to be done is to add this module to the graph during the initialization stage:

 public class TweeterApp { public static void main(String... args) { Injector.init(new NetworkModule(), new MockNetworkModule()); Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world"); Timeline timeline = new Timeline("Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) { System.out.println(tweet); } } } 


Now all our requests will go through the test Http client.

This is not all the features of Dagger'a - I described only one of the possible scenarios for using this library. In any case, a thoughtful reading of the documentation is indispensable.

This is what happened in the end (the same as above, but collected in a heap)
 //Entry point   public class TweeterApp { public static void main(String... args) { Injector.init(new NetworkModule()); Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world"); Timeline timeline = new Timeline("Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) { System.out.println(tweet); } } } //  public class Injector { public static ObjectGraph graph; public static void init(Object... modules) { graph = ObjectGraph.create(modules); } public static void inject(Object target) { graph.inject(target); } } //, Tweeter (   HttpClient  ) public class Tweeter { private TwitterApi api; public Tweeter() { this.api = new TwitterApi(); } public void tweet(String tweet) { api.postTweet("Test User", tweet); } } //TwitterApi,   HttpClient  Dagger'a public class TwitterApi { @Inject private HttpClient client; public TwitterApi() { //     Injector.inject(this); //   ""  client  Dagger' } public void postTweet(String user, String tweet) { HttpUrlConnection connection = client.open("...."); /* post tweet */ } } //,   HttpClient ,     ( ""   'injects' ) @Module(injects=TwitterApi.class) public class NetworkModule { @Provides @Singleton HttpClient provideHttpClient() { return new OkHttpClient(); } } 



List of useful materials on the topic:

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


All Articles