This lesson will provide you with detailed information about Dagger and what it can do. We will discuss the notions of a component and module, connect Dagger to a project and look at a couple of simple examples.

 

What is Dagger for?

If you would like to decrease the inter-object dependency and simplify the procedure of developing tests for your code, the Dependency Injection is exactly what you want. Dagger, on the other hand, is a library that will help to implement the pattern. In this mini training course I will describe how to use the Dagger library version 2 (we will refer to it simply as Dagger).

The advantages of Dagger as opposed to the other libraries out there are as follows:
- it helps to generate code that is easy to understand and debug;
- it checks dependencies at compilation stage;
- it does not create any issues, when proguard is used.

I can tell you right away that this is quite a challenging topic, and questions like “and what would happen if I did this” are expected. Unfortunately, I cannot discuss all the possible cases, which is why I strongly recommend that you create examples and run tests on how different things work under different circumstances using those examples. In my experience, real-life examples have significantly helped me understand the theory better.

In order to understand why it is that we might need Dependency Injection and Dagger, let's consider a small abstract example. We will model a situation, where we create one object and that entails creation of several more objects.

Say, we have a certain MainActivity in our application. And, according to the MVP pattern, we have a presenter for such MainActivity. In order for the presenter to work, we would need a certain ItemController and DataController. That means that we will have to create two of these objects before we create the presenter. However, in order to create these two objects, we would need the ApiService and SharedPreferences objects. And we cannot have the ApiService without RestAdapter, RestAdapter.Builder, OkHttpClient and Cashe.

In a standard implementation, this could look as follows:

public class MainActivity extends Activity {

    MainActivityPresenter activityPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File cacheDirectory = new File("some path");
        Cashe cashe = new HttpResponseCache(cacheDirectory, 50 * 1024 * 1024);

        OkHttpClient httpClient = new OkHttpClient();
        httpClient.setCache(cashe);

        RestAdapter.Builder builder = new RestAdapter.Builder();
        builder.setClient(new OkClient(httpClient));
        RestAdapter restAdapter = builder.build();
        ApiService apiService = restAdapter.create(ApiService.class);

        ItemController itemController = new ItemController(apiService);

        SharedPreferences preference = getSharedPreferences("item_prefs", MODE_PRIVATE);
        DataController dataController = new DataController(preference);

        activityPresenter = new MainActivityPresenter(this, itemController, dataController);
    }

}

We create a lot of objects in MainActivity in order to simply get one presenter. In this example we do not really care which objects are being created. The main thing is how much code would need to be written in the MainActivity for us to get the desired outcome.

If we apply the Dependency Injection pattern and use Dagger, the code in the Activity would look like this:

public class MainActivity extends Activity {

    MainActivityPresenter activityPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        activityPresenter = App.getComponent().getPresenter();
    }
}

Of course, the code for creating the objects did not go anywhere. But it has been moved from the Activity to individual classes, which Dagger can access. As a result, we just call the getPresenter method to get the MainActivityPresenter object. And Dagger will then go ahead and create that object and the entire hierarchy of objects required for it.

We could do the same without Dagger by simply moving the code for creating the objects into a method like MainActivityPresenter.createInstance(). But if we have a different presenter, which partially needs the same objects in the createInstance method for such other presenter, we would have to duplicate the code for creating certain objects.

If we use Dagger, the code for creating the required object would only exist in one place and as one instance. And Dagger would then use that code everywhere we need to create an object.

 

 

Theory

Now, let's look at how Dagger works from the inside.

Let's consider the same example with Activity and Presenter, where Activity creates the Presenter object for its purposes. The regular creation algorithm would look as follows:

Activity -> Presenter

Here we see that Activity creates Presenter by itself.

 

If we use Dagger, the algorithm would looks as follows:

Activity -> Component -> Module -> Presenter

The Activity accesses the component, and the component then uses the modules to create the Presenter and return it to the Activity.

 

Modules and components are the two key notions in Dagger.

 

Modules are simply classes, where we put the code used to create the objects. Normally each module comprises objects that are similar in their purpose. For example:

The ItemModule would contain the code used to create the objects associated with users, that is, something like Item and ItemController.

The NetworkModule includes the OkHttpClient and ApiService objects.

The StorageModule consists of the DataController and SharedPreferences objects.

 

Component, on the other hand, is agent between the Activity and the modules. When the Activity needs a certain object, it accesses the component. The component knows which module can be used to create the object, so it requests that the module create the object and return it to the Activity. Note that the component can use other modules to create the entire hierarchy of objects required to generate the desired object.

 

We can actually compare the process of how Dagger works with a lunch at McDonald's. So, if we use the Dagger algorithm as analogy:

Activity -> Component -> Module -> Presenter

the McDonald's lunch algorithm would look as follows:

Customer -> Cashier -> Production Line -> Order (Big Mac/Fries/Coke).


Let's look at the steps of those algorithms in more detail:

McDonalds Dagger
The customer has decided to order a Big Mac, fries and Coke. So he places his order with a cashier The Activity tells the component that it would need a Presenter
The cashier then goes to the production line and puts the order together: he takes a Big Mac, pours a glass of Coke, puts the fries onto the tray The component goes through the modules and creates all the objects required to create the Presenter
Then the Cashier places all the items into the bag and hands the bag over to the customer The component creates the required object - Presenter - and provides it to the Activity

 

 

Real Life Examples

Now, let's consider a simple real-life example of how to create modules and components, and how the Activity would use those modules and components to receive the required objects.

 

Connecting Dagger to a Project

Create a new project. In order to be able to use Dagger, add the following lines at the end of the build.gradle file in your module:

// Add plugin https://bitbucket.org/hvisser/android-apt
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

// Apply plugin
apply plugin: 'com.neenbedankt.android-apt'

// Add Dagger dependencies
dependencies {
    compile 'com.google.dagger:dagger:2.7'
    apt 'com.google.dagger:dagger-compiler:2.7'
}

If for some reason something is not working right, you can download the ready-to-use work project here.

 

We will use the following two classes as the objects that we will request from Dagger: DatabaseHelper and NetworkUtils.

public class DatabaseHelper {
  
}

 

public class NetworkUtils {

}

We do not care about their implementation right now, so we just leave them blank.

 

Let's say, we need these objects in the MainActivity

public class MainActivity extends Activity {

    DatabaseHelper databaseHelper;
    NetworkUtils networkUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

In order to get them using Dagger, we will need to create the modules and component.

 

So we go ahead and create modules that would know how to provide the required objects. It is precisely in the modules that we write the entire code used to create the objects. These are regular classes, but with a number of annotations:

@Module
public class NetworkModule {

    @Provides
    NetworkUtils provideNetworkUtils() {
        return new NetworkUtils();
    }

}

 

@Module
public class StorageModule {

    @Provides
    DatabaseHelper provideDatabaseHelper() {
        return new DatabaseHelper();
    }

}

We use the @Module annotation to tell Dagger that this class is a module, while the @Provides annotation indicates that this method can supply an object, and the component can use it to get that object. From the technical standpoint, we could pretty much manage with just one module. But it would be much more logical to break down the object into modules based on their intended application and purpose.

Now that the modules are ready, let's create the component. In order to do that, we need to create an interface.

 

@Component()
public interface AppComponent {

}

This interface describes a blank component, which for now will know how to do nothing. When the project is compiled, Dagger will find that interface using the @Component annotation and generate the DaggerAppComponent class, which implements this interface. This would be the component class.

All we need to do is fill the interface with methods. By doing so we will let the component know, which objects it has to be able to return to us. And when the project gets compiled, Dagger itself will implement them in the generated component class.

The component can return objects to us in one of the two ways. The first way is the regular get methods. This means that we simply call the method, which returns us the object. The second way is more interesting and involves using the inject methods. In this case we provide the component with an instance of the Activity, and the component then fills in all the required fields by itself creating the required objects.

Let's consider these two options using examples:

 

Get methods

Let's complement the interface, so that the component would know how to create objects for us.

@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    NetworkUtils getNetworkUtils();
    DatabaseHelper getDatabaseHelper();
}

The modules list includes the modules, in which the component can search for the code required to create objects.

Methods can have various names. The most important thing is the types returned by such methods (NetworkUtils and DatabaseHelper). They let the component know, which objects it is that we want it to return to us. At the time of compilation Dagger will check, which object can be taken from which module and will generate the respective code for creating these objects in implementation of these two methods. While we will simply call these component methods in the MainActivity in order to get the finished objects.

 

All that's left is to describe the creation of a component instance somewhere. And we will use the Application class to do that. Don't forget to add it to the manifest.

public class App extends Application {

    private static AppComponent component;

    @Override
    public void onCreate() {
        super.onCreate();
        component = DaggerAppComponent.create();
    }

    public static AppComponent getComponent() {
        return component;
    }

}

Now, we create the component in the onCreate method. At this point your development framework will most likely get upset about the Dagger AppComponent class. This happens because the Dagger AppComponent class does not exist quite yet. We have just described the interface of the AppComponent class, but we need to compile the project for Dagger to create this component class.

Compile the project. If you are using Android Studio, you can do it from the following menu: Build -> Make Project (CTRL+F9). Once the process is complete, the Dagger AppComponent class will be created in the build\generated\ folder. The Studio now knows this class and should be offering you to add it to import, so that the code would contain no errors.

 

We can now use this component in the MainActivity, in order to get the ready-to-use DatabaseHelper and NetworkUtils objects.

public class MainActivity extends Activity {

    DatabaseHelper databaseHelper;
    NetworkUtils networkUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        databaseHelper = App.getComponent().getDatabaseHelper();
        networkUtils = App.getComponent().getNetworkUtils();
    }
}

 

Once we run the application, Dagger will create the objects. If the whole thing is crashing with an NPE, make sure that you have added the App class to the manifest.


 

Inject methods

As of right now we have two objects in the MainActivity, and we receive those from the component. But if instead there were 20 objects, we would have to describe 20 get methods in the component interface and write 20 calls for such methods in the MainActivity code. Luckily for us, Dagger offers a more convenient solution for cases like that. We can teach the component to not only return objects, but to also fill the Activity with the required objects by itself. That means that we would give the component an instance of the MainActivity, and the component would then decide which objects are needed, create them and place them into the respective fields, all by itself.

Let's rewrite the component interface:

@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    void injectsMainActivity(MainActivity mainActivity);
}

We will describe one inject method instead of a couple of get methods. The name can be anything you want. The main thing is the type of its only parameter. Here we indicate the MainActivity. This way we are telling the component that when we call this method and provide it with the MainActivity instance, we expect that the component would fill the instance with the required objects.

When the project gets compiled, Dagger would see this method in the interface, go through the MainActivity class in search of the fields (marked with special annotations) and determine which objects it would need to create. As a result, Dagger implements the injectsMainActivity method in the component class in order for the component to receive objects from its modules and insert them into the respective variables of the MainActivity instance returned to it.

 

Let's rewrite MainActivity:

 

public class MainActivity extends Activity {

    @Inject
    DatabaseHelper databaseHelper;

    @Inject
    NetworkUtils networkUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        App.getComponent().injectsMainActivity(this);
    }
    
}

We use the @Inject annotations to mark the fields, which the component needs to fill in. When the injectsMainActivity is called, the component will take the DatabaseHelper and NetworkUtils objects from the modules and place them into the MainActivity fields.

We can see this mechanism in the component class code generated by Dagger. The injectsMainActivity method looks as follows:

    public void injectsMainActivity(MainActivity mainActivity) {
        mainActivityMembersInjector.injectMembers(mainActivity);
    }

 

If we go further and look inside mainActivityMembersInjector.injectMembers, we will see the following:

    @Override
    public void injectMembers(MainActivity instance) {
        if (instance == null) {
            throw new NullPointerException("Cannot inject members into a null reference");
        }
        instance.databaseHelper = databaseHelperProvider.get();
        instance.networkUtils = networkUtilsProvider.get();
    }

What we see here is simply the null check and assignment of objects to the MainActivity fields.

 

It goes without saying that both, the get methods and the inject methods can be used together within one component. I have described them separately for clarity purposes only.

 

 

Dependency Graph

The total of all the objects, which the component can generate, is referred to as the component object graph or component dependency graph. If we look at the example above, that graph consists of only two objects: DatabaseHelper and NetworkUtils. The component knows how to create these objects and can provide them.

In some cases, when the component creates one object, it might require another object. We have already talked about that at the very beginning of this lesson, when in order to create one presenter, we had to create another dozen of objects.

Let's look at an example of a module.

 

@Module
public class NetworkModule {

    @Provides NetworkUtils provideNetworkUtils(HttpClient httpClient) {
        return new NetworkUtils(httpClient);
    }

    @Provides HttpClient provideHttpClient() {
        return new HttpClient();
    }

}

When we request the component to provide the NetworkUtils object, the component will go to that module and call the provideNetworkUtils method. But that method needs the HttpClient object to enter. The component then looks for such a module within the component that would be able to create such object and finds the required object within such module. It calls the provideHttpClient method, gets the HttpClient object and uses it to call the provideNetworkUtils method. What this means is, that if an object needs other objects for it to be created, you will have to describe creation of all these objects in the modules. In this case the component will create the entire chain and get the desired object.

There are cases, when not everything can easily be created in the modules and some objects might be required from outside Dagger. I will describe a case like that in one of my future lessons.

 

Detection of Errors

One of the advantages of using Dagger is that if you have some kind of error in building dependencies, you will find out about it at the stage of compilation, rather than in Runtime. Let's check that. We will create another blank class called Preferences

public class Preferences {
    
}

 

We will also add a variable of this type with the Inject annotation to the MainActivity:

@Inject
Preferences preferences; 

Now, if we call the inject method, the component must create the Preferences object, but we have not added a description of how this object is created into the modules. So the component simply does not know where to get this object.

Let's try and compile the project. We will get this error:
Error: (24, 10) error: Preferences cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.

The compiler quite logically complains that it does not know where the component would take the Preferences object from.

 

What Next?

In this lesson we have talked about the basics of working with Dagger. We have not touched upon any specific details or difficulties. After you read it, you will be able to understand the general principle of how Dagger works and the role of the component and modules in it. In the next lessons we will talk about additional features of creating objects offered by Dagger. We will discuss one example of how components can be organized in a small application. We will also explore the ability of the component to create objects in a separate thread.

 


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня



Language