依賴注入(Dependency Injection, DI)在近年來是一個相當熱門的技術。隨著程式碼越來越大,我們必須要有一項技術來幫助我們管理程式碼的架構,並保持各個模組(Module)間的去耦合(Decoupling)。在 Android 中,Dagger 2 是最常用的 DI framework。
Table of Contents
依賴注入
使用依賴注入時,所有要依賴注入的物件都要有相對應的 Interface。如下圖中,OrderManager 和 OrderRepository 都是 Interface。所以我們可以在 runtime 時,注入實作的物件,也就是 OrderManagerImpl 和 OrderRepositoryImpl。
以下是它們的程式碼。
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 會是更好的選擇。
參考
- Dagger basics, Android Developers
- Using Dagger in Android apps, Android Developers
- Dagger 2 Tutorial For Android: Advanced, raywenderlich.com