Dependency Injection (DI) has been a very popular technique in recent years. As the code becomes larger, we must have a technology to help us manage the structure of the code and maintain the decoupling between modules. In Android, Dagger 2 is the most commonly used DI framework.
The complete code for this chapter can be found in .
Table of Contents
Dependency Injection
When using dependency injection, all objects to be injected must have a corresponding Interface. As shown in the figure below, both OrderManager and OrderRepository are Interface. So we can inject the implementation at runtime that are OrderManagerImpl and OrderRepositoryImpl.
The following is their code.
data class Order( val name: String, val price: Double, )
interface OrderRepository { fun findAll(): List<Order> }
class OrderRepositoryImpl : OrderRepository { override fun findAll(): List<Order> = listOf( Order("CocoCola", 1.5), Order("Fries", 1.25), Order("Burger", 5.59), ) }
interface OrderManager { fun getList(): List<Order> }
class OrderManagerImpl(private val repository: OrderRepository) : OrderManager { override fun getList(): List<Order> = repository.findAll() }
We can implement dependency injection manually.
val repository = OrderRepositoryImpl() val manager = OrderManagerImpl(repository) val orders = manager.getList()
Dagger 2
In the previous section, we saw that dependency injection can be implemented manually. However, it will be very troublesome if you have to manually implement it every time you need to use it. Dagger 2 can help us simplify this procedure.
Before we start to use Dagger 2, we first need to add the following dependencies into the module level app/build.gradle.
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.40.5' kapt 'com.google.dagger:dagger-compiler:2.40.5' }
@Module & @Provides
Next, we need to declare a @Module class to tell Dagger which interfaces need to instantiate and how to instantiate those classes. @Module contains a bunch of @Provides methods. The return type of @Provides method is the interface to be injected, and its return value is the implementation of that interface. All in all, @Module and @Provides provide a dependency graph.
In the following code, for the interface OrderRepository, we choose to use OrderRepositoryImpl as its implementation.
import dagger.Module import dagger.Provides @Module class OrderModule { @Provides fun orderRepository(): OrderRepository = OrderRepositoryImpl() @Provides fun orderManager(repository: OrderRepository): OrderManager = OrderManagerImpl(repository) }
@Component & @Inject
@Inject tells Dagger the target to inject. In the following code, we tell Dagger that we want to inject OrderManager.
class OrderFirstFragment : Fragment() { ... @Inject lateinit var orderManager: OrderManager ... }
We also declare @Component interface to tell Dagger where to inject and which module to use for instantiating the implementation. In the following code, we declare the interface OrderComponent. Note that it must be an interface, because Dagger will automatically generate class DaggerOrderComponent afterwards.
import dagger.Component @Component(modules = [OrderModule::class]) interface OrderComponent { fun inject(fragment: OrderFirstFragment) }
Using Dagger to Inject Objects
So far, we have declared @Module and @Component, and we have also specified to inject OrderManager in OrderFirstFragment. But these only tell Dagger which objects to inject, where to inject, and how to instantiate the implementations. Dagger only knows this information, we have to tell it when to inject.
We declared an interface OrderComponent, Dagger will automatically generate class DaggerOrderComponent. Usually we instantiate the @Component in Application, so that all Activity in the app can access. But in order to simplify the code, in the example, we instantiate the OrderComponent instance in OrderActivity.
class OrderActivity : AppCompatActivity() { ... lateinit var orderComponent: OrderComponent override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) orderComponent = DaggerOrderComponent.create() ... } ... }
Then in OrdreFirstFragment, we get OrderComponent, and the call inject()
to perform the injection to OrderFirstFragment.
class OrderFirstFragment : Fragment() { ... @Inject lateinit var orderManager: OrderManager override fun onAttach(context: Context) { activity?.let { (it as OrderActivity).orderComponent.inject(this) Log.d(javaClass.canonicalName, "orderManager: $orderManager") } super.onAttach(context) } ... }
The basic usage of Dagger is roughly as above. Compared with the manual method, it is actually similar.
Scope Management: @Singleton
In the previous section, we found that in OrderFirstFragment, the instances of OrderManager injected each time are different entities. When we want OrderManager to have only one instance in the app, we can use @Singleton to tell Dagger to instantiate the OrderManager only once.
Open OrderModule, add @Singleton to orderRepository()
and orderManager()
. Because we hope that in the app, both OrderRepository and OrderManager need only one instance.
import dagger.Module import dagger.Provides import javax.inject.Singleton @Module class OrderModule { @Singleton @Provides fun orderRepository(): OrderRepository = OrderRepositoryImpl() @Singleton @Provides fun orderManager(repository: OrderRepository): OrderManager = OrderManagerImpl(repository) }
We must also add @Singleton to OrderComponent as well.
import dagger.Component import javax.inject.Singleton @Singleton @Component(modules = [OrderModule::class]) interface OrderComponent { fun inject(fragment: OrderFirstFragment) fun inject(fragment: OrderSecondFragment) }
That’s it. You will find that the OrderManager injected into OrderFirstFragment by Dagger is the same entity every time .
@Binds
In the above example, we put all the information to be provided to Dagger in @Module and @Component. Dagger provides another way to provide these information, that is @Binds . @Binds will help us automatically generate the code for instantiating implementation in @Module.
Below we will use another example Product to show how to use @Binds. The structure of the example Product is almost the same as the example Order.
data class Product( val name: String, val price: Double, )
interface ProductRepository { fun findAll(): List<Product> }
interface ProductManager { fun getAll(): List<Product> }
The difference with OrderRepositoryImpl is that we must add @Inject before the constructor of ProductRepositoryImpl, even if the constructor does not have any parameters, we must add @Inejct. If you want ProductRepositoryImpl to have only one entity in the app, add @Singleton to it.
import javax.inject.Inject import javax.inject.Singleton @Singleton class ProductRepositoryImpl @Inject constructor() : ProductRepository { override fun findAll(): List<Product> = listOf( Product("Chocolate", 1.0), Product("Jelly Bean", 0.2), ) }
There is a parameter of ProductRepository in the constructor of ProductManagerImpl, and then Dagger will automatically pass the parameter ProductRepository when instantiate ProductManagerImpl.
import javax.inject.Inject import javax.inject.Singleton @Singleton class ProductManagerImpl @Inject constructor(private val repository: ProductRepository) : ProductManager { override fun getAll(): List<Product> = repository.findAll() }
Now we will declare @Module as the following code. As you can see, ProductModule is quite clean compared to OrderModule. It is worth noting that class ProductModule must be abstract. Because then Dagger will help us generate the code for all methods.
In ProductModule, we declare two methods, one of which is productManager()
. We add @Binds to it, telling Dagger that this method uses the type ProductManagerImpl of its parameter as the implementation of the return type ProductManager.
import dagger.Binds import dagger.Module @Module abstract class ProductModule { @Binds abstract fun productRepository(productRepository: ProductRepositoryImpl): ProductRepository @Binds abstract fun productManager(productManager: ProductManagerImpl): ProductManager }
Then, we also have to declare @Component.
import dagger.Component import javax.inject.Singleton @Singleton @Component(modules = [ProductModule::class]) interface ProductComponent { fun inject(fragment: ProductFirstFragment) fun inject(fragment: ProductSecondFragment) }
The usage of the example Product is the same as the example Order.
class ProductActivity : AppCompatActivity() { ... lateinit var productComponent: ProductComponent override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) productComponent = DaggerProductComponent.create() ... } ... }
class ProductFirstFragment : Fragment() { ... @Inject lateinit var productManager: ProductManager override fun onAttach(context: Context) { activity?.let { (it as ProductActivity).productComponent.inject(this) Log.d(javaClass.canonicalName, "productManager: $productManager") } super.onAttach(context) } ... }
We see that when using @Binds, we have to add @Inject and @Singleton to ProductManagerImpl. In other words, ProductManagerImpl must know Dagger. If ProductManagerImpl is in another module, using @Binds is a bad practice.
Conclusion
When your project becomes larger, you will need to dependency injection to maintain the structure and cleanliness of the code. In simple cases, dependency injection may be implemented manually. However, when there are many objects that need to be injected, perhaps Dagger is a better choice.
Reference
- Dagger basics, Android Developers
- Using Dagger in Android apps, Android Developers
- Dagger 2 Tutorial For Android: Advanced, raywenderlich.com