Android:Dagger 2 依賴注入

Photo by Zeke See on Unsplash
Photo by Zeke See on Unsplash
依賴注入在近年來是一個相當熱門的技術。隨著程式碼越來越大,我們必須要有一項技術來幫助我們管理程式碼的架構,並保持各個模組間的去耦合)。在 Android 中,Dagger 2 是最常用的 DI framework。

依賴注入(Dependency Injection, DI)在近年來是一個相當熱門的技術。隨著程式碼越來越大,我們必須要有一項技術來幫助我們管理程式碼的架構,並保持各個模組(Module)間的去耦合(Decoupling)。在 Android 中,Dagger 2 是最常用的 DI framework。

本章完整的程式碼可以在 下載。

依賴注入

使用依賴注入時,所有要依賴注入的物件都要有相對應的 Interface。如下圖中,OrderManager 和 OrderRepository 都是 Interface。所以我們可以在 runtime 時,注入實作的物件,也就是 OrderManagerImpl 和 OrderRepositoryImpl。

Order Diagram
Order Diagram

以下是它們的程式碼。

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()
}

我們可以用手動的方式來實現依賴注入。

val repository = OrderRepositoryImpl()
val manager = OrderManagerImpl(repository)
val orders = manager.getList()

Dagger 2

上一小節,我們看到可以用手動的方式來實現依賴注入。但,如果每次要用的時候,都要手動來實作的話,就會很麻煩。Dagger 2 可以幫我們簡化這一段的程式碼。

在開始使用 Dagger 2 之前,我們要先將以下的 dependencies 加入到 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

接下來,我們要宣告一個 @Module class 來告訴 Dagger 需要 instantiate 哪些 interfaces,以及要怎麼 instantiate 那些 classes。@Module 裡面包含了一堆 @Provides methods。@Provides method 的 return type 就是要依賴注入的 interface,而它的 return value 就是那個 interface 的 implementation。總而言之,@Module 和 @Provides 提供了依賴圖(dependency graph)。

下面的程式碼中,對於 interface OrderRepository,我們選擇用 OrderRepositoryImpl 作為它的 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 告訴 Dagger 要 inject 的目的地。如下程式碼中,我們告訴 Dagger 我們想要 inject OrderManager。

class OrderFirstFragment : Fragment() {
    ...

    @Inject
    lateinit var orderManager: OrderManager

    ...
}

我們還要宣告 @Component interface 來告訴 Dagger 要 inject 到哪裡,以及要使用哪個 module 來 instantiate implementation。在以下的程式碼中,我們宣告了 interface OrderComponent。注意到它必須是 interface,因為之後 Dagger 會自動產生 class DaggerOrderComponent。

import dagger.Component

@Component(modules = [OrderModule::class])
interface OrderComponent {
    fun inject(fragment: OrderFirstFragment)
}

使用 Dagger 來注入物件

至目前為止,我們已經宣告了 @Module 和 @Component,並且也在 OrderFirstFragment 中指定要 inject OrderManager。但這些都只是告訴 Dagger 要 inject 哪些 objects,要 inject 到哪裡,以及要怎麼 instantiate 那些 implementation。Dagger 只是知道這些資訊,我們還要告訴它什麼時候 inject。

我們宣告了一個 interface OrderComponent,Dagger 會自動產生 class DaggerOrderComponent。通常我們會在 Application 中 instantiate @Component 的實例,這樣程式中所有的 Activity 都可以存取到。不過為了簡化程式碼,在範例中,我們在 OrderActivity 裡 instantiate OrderComponent 的實例。

class OrderActivity : AppCompatActivity() {
    ...

    lateinit var orderComponent: OrderComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        orderComponent = DaggerOrderComponent.create()

        ...
    }
    ...
}

然後在 OrdreFirstFragment 中,我們取得 OrderComponent,並且呼叫 inject() 來對 OrderFirstFragment 執行 injection。

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)
    }

    ...
}

整個 Dagger 的基本使用方法大致如上。與手動的方式相比起來,其實相差不多。

Scope 管理:@Singleton

在上一小節中,我們發現在 OrderFirstFragment 中,每次注入的 OrderManager 的實體都是不同的實體。當我們希望 OrderManager 在 app 中只有一個實體時,我們可以用 @Singleton 來告訴 Dagger 對 OrderManager 只要 instantiate 一次。

打開 OrderModule,在 orderRepository()orderManager() 都加上 @Signleton。因為我們希望在 app 中,OrderRepository 和 OrderManager 都只要一個實體。

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)
}

我們還必須也要在 OrderComponent 上加上 @Singleton。

import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(modules = [OrderModule::class])
interface OrderComponent {
    fun inject(fragment: OrderFirstFragment)

    fun inject(fragment: OrderSecondFragment)
}

這樣就大功告成。你會發現,Dagger 每次注入到OrderFirstFragment 中的 OrderManager 都是同一個實體。

@Binds

以上範例中,我們將所有要提供給 Dagger 的資訊都放在 @Module 和 @Component 裡面。Dagger 提供了另外一種寫法,那就是 @Binds。@Binds 會幫我們自動產生 @Module 裡面那些 instantiate 實作的程式碼。

以下我們將使用另外一個範例 Product 來展示如何使用 @Binds。範例 Product 的結構與範例 Order 幾乎是相同。

data class Product(
    val name: String,
    val price: Double,
)
interface ProductRepository {
    fun findAll(): List<Product>
}
interface ProductManager {
    fun getAll(): List<Product>
}

與 OrderRepositoryImpl 不同是的是,我們必須要在 ProductRepositoryImpl 的建構子前面加上 @Inject,即使建構子沒有任何參數也是要加上 @Inejct。如果你希望 ProductRepositoryImpl 在 app 中只有一個實體時,就在它上面加上 @Singleton。

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),
    )
}

ProductManagerImpl 的建構子中有一個 ProductRepository 的參數,之後 Dagger 會在 instantiate ProductManagerImpl 時,自動幫我們帶入參數 ProductRepository。

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()
}

現在我們將宣告 @Module,如下程式碼。你可以看到,與 OrderModule 相比,ProductModule 相當地簡潔。值得注意的是,class ProductModule 必須是 abstract。因為之後 Dagger 會幫我們產生所有 methods 的程式碼。

在 ProductModule,我們宣告了兩個 methods,其中一個是 productManager()。我們在它的上面加上了 @Binds,告訴了 Dagger 此 method 將它的參數的型態 ProductManagerImpl 作為 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
}

然後,我們還要宣告 @Component。

import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(modules = [ProductModule::class])
interface ProductComponent {
    fun inject(fragment: ProductFirstFragment)

    fun inject(fragment: ProductSecondFragment)
}

範例 Product 的使用方式與範例 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)
    }
    ...
}

我們看到使用 @Binds 寫法時,我們要在 ProductManagerImpl 上加上 @Inject 和 @Singleton。也就是說 ProductManagerImpl 必須要知道 Dagger。如果 ProductManagerImpl 是在另外一個模組時,@Binds 的寫法也就會是所謂的 bad practice。

結語

當你的專案越來越龐大時,勢必要引入依賴注入來幫助你維護程式碼的架構與整潔。在簡單的情況下,也許可以手動實作依賴注入。但,當需要依賴注入的物件很多的時候,也許借助於 Dagger 會是更好的選擇。

參考

  1. Dagger basics, Android Developers
  2. Using Dagger in Android apps, Android Developers
  3. Dagger 2 Tutorial For Android: Advanced, raywenderlich.com
發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More