Dependency injection in Android

Tram Ho

1. Introduction

In the previous section I gave an overview of what Dependency Injection is. It is mentioned that the implementation of DI can be manual (manual) or automatic. In this article I will share how to use DI technology in an Android application manually as well as its advantages and disadvantages ^^

2. Application graph

The proposed Android application architecture encourages dividing your code into separate modules and modules to benefit from separation of concerns, there is a basic principle that each class of the hierarchy has a responsibility. The mission is determined to be unique. This split will result in many smaller layers that need to be connected to each other to implement each other’s dependencies. Below is the chart model of the recommended android Android application Dependencies between classes can be represented in the form of a graph, in which each class is connected to the classes it depends on. The representation of all your classes and the dependencies that hold them makes up the application graph. As the diagram above you can see its abstraction. When class A (ViewModel) depends on class B (Repository), there will be an arrow from A to B indicating that dependency.

Dependency injection helps to make these connections and allows us to swap deployments for testing. For example, when testing the ViewModel, the dependent class and the Repository, we can easily test Repository implementations with fakes or mocks to test different cases.

3. Basic of manual dependency injection

This section will guide Dependency injection manually in real Android applications. Will tell you how to use DI in your application. These manual approaches will give you an understanding of the nature of Dagger, but later you can use it automatically.

Consider a thread as a group of screens within an application that serve a feature. Login, Registration, checkout are flow examples.

A login flow of a regular Android application, LoginActivity depends on LoginViewModel, LoginViewModel depends on UserRepository. Then UserRepository depends on UserLocalDataSource and UserRemoteDataSource, UserRemoteDataSource depends on Retrofit Service, … LoginActivity is the input of the login stream, and is where users interact. Therefore, LoginActivity needs to create LoginViewModel with all its dependencies.

The Repository and DataSource classes will look like this

Kotlin:

Java:

Here is the code in LoginActivity:

Kotlin:

Java:

With this old approach there would be the following problem:

  1. There is a lot of code written down. If we want to create a LoginViewModel in another place we have to duplicate the above code.
  2. Dependencies must be ordered in order to run. We must create Retrofit before RemoteDataSource, must create DataSource before UserRepository, must create UserRepository before LoginViewModel.
  3. It is difficult to reuse objects. If you want to reuse the UserRepository on many other features, you must let it follow the singleton pattern. But the singleton pattern makes testing difficult because all test cases are created only by the UserRepository object.

4. Managing dependency with a container

To solve the problem of reusing the above object, we can create a container dependencies class to use to contain the dependencies. All instances provided by this class can be made public. In the above example we only need an instance of UserRepository and can use many places. You can set private dependencies and when provided, switch to public. Kotlin:

Java:

Because these dependencies are used throughout the application, they need to be placed in a common place that all activities can use: Application class. Create a custom application class that contains an instance of AppContainer.

Kotlin:

Java:

Note that AppContainer is just a regular class with a single instance shared and placed in Application Class. And it does not follow the singleton pattern (Not an object in Kotlin, and Java, not accessed using the typical Singleton.getInstance () method)

Now we can get the AppContainer instance from the application and get the share of the UserRepository instances.

Kotlin:

Java:

In this example you do not have a Singleton of UserRepository. Instead you have a shared AppContainer for the Activity. If LoginViewModel is needed in many places in the application. There is a centralized place where you create its instance. We can create a LoginViewModel in the container and supply new objects according to the Factory.

Kotlin:

Java:

Then we can declare LoginViewModelFactory in AppContainer and get LoginViewModel in LoginActivity.

Kotlin:

Java:

These approaches are better than the method on which there are still a few things to consider:

  1. You must manage AppContainer yourself, create instances of dependencies manually.
  2. There is still a lot of code to write. You need to create factories and parameters manually depending on whether you want to use an object or not.

5. Managing dependencies in application flows

AppContainer will become cumbersome when the application has many functions. When a large application starts to perform the same steps again, there are even many things that arise:

  1. When you have different feature streams, you may also want objects that only live within that region. For example, in case you create a LoginUserData (username and password), you don’t want to store data from the old login stream from another user. You need a new data for a new thread. You can achieve that by creating a FlowContainer object in AppContainer (example below)
  2. Optimizing Application Graph and flow containers can be difficult.

Let’s create a LoginContainer. We can create multiple LoginContainer instances in the application if desired, so instead of turning it into a Singleton, turn it into a class with the dependencies that the login flow needs from AppContainer.

Kotlin:

Java:

When you have a Container for a Flow, you must decide when to create and delete the instance of the Container. Because the login stream is closed in ActivityLogin. So we create the instance in onCreate () and delete it in onDestroy ().

Kotlin:

Java:

6. Conclusion

Dependency injection is a good technique for creating scalable and easy to test Android applications. Using containers as a way to share instances of different parts of the application, or in other words it is a central place to create instances of classes (using factories).

As your application gets bigger, you will start to see that you have to write a lot of code (like factories), so it can be error-prone. You also have to manage the scope and lifecycle of the containers yourself, optimize and eliminate unnecessary containers to free up memory. Failure to do so could result in very sophisticated errors or memory leaks in your application. This is the downside of using Dagger manually

In the next part, I will share how to use Dagger automatically. This shared knowledge I refer to from Google, thank you for following the article.

Share the news now

Source : Viblo