今天要先來將專案全面替換成 Dagger ,現在可以先把 Data layer 及 部分 Presentation layer 的 class 換掉。
先來看看 TasksRepository
:
class TasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ITasksRepository {
......
}
可以看到 TasksRepository
主要依賴了 TasksLocalDataSource
及 TasksRemoteDataSource
。
因此如果要讓 Dagger 可以提供 TasksRepository
的話,我們要先讓 Dagger 可以提供 TasksDataSource
的依賴:
@Module
object ApplicationModule {
......
@Provides
fun provideTasksRemoteDataSource(): TasksDataSource {
return TasksRemoteDataSource
}
@Provides
fun provideTasksLocalDataSource(tasksDao: TasksDao): TasksDataSource {
return TasksLocalDataSource(tasksDao)
}
}
class TasksLocalDataSource @Inject constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource {
......
}
class TasksRepository @Inject constructor(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ITasksRepository {
......
}
此時我們發覺 TasksRepository
其實需要的是 TasksDataSource
的兩個不同的實作體,但這時候會發現 Dagger 因為有多種方式可以取得同一個依賴而不知道該依賴哪個實體而崩潰了。
這個情況有個說法叫做依賴迷失,我們該如何處理呢?
這時我們就要用到 @Qualifier
了。
Dagger 提供了一個叫做 Qualifier(限定符)
的 annotation , 其思路就是透過建立不同的註解,並使用他們標記依賴的地方,讓 Dagger 可以知道這時候他要提供哪個依賴物給對方。
@Module
object ApplicationModule {
......
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class TasksLocalData
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class TasksRemoteData
@TasksRemoteData
@Provides
fun provideTasksRemoteDataSource(): TasksDataSource {
return TasksRemoteDataSource
}
@TasksLocalData
@Provides
fun provideTasksLocalDataSource(tasksDao: TasksDao): TasksDataSource {
return TasksLocalDataSource(tasksDao)
}
}
/**
* 在這裏標記 Qualifier 讓 Dagger 知道這時候他該提供哪個依賴給 TasksRepository
*/
class TasksRepository @Inject constructor(
@TasksRemoteData private val tasksRemoteDataSource: TasksDataSource,
@TasksLocalData private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ITasksRepository {
......
}
前幾天有提到 ViewModelFactory
,那麼現在就著手改造吧。
這裏的做法跟前面有一點不同,我們要使用 Dagger 的 Multibindings 來處理, Multibindings 簡單的說就是利用 Map
鍵對的特性達到多個對象綁定到一個集合的功能,讓依賴者可以透過集合得到依賴物而不是經由提供依賴的方法本身。
那麼為什麼要改成這樣呢?
原本的 ViewModelFactory
的寫法,隨著內部的 ViewModel 種類的增加,每此都要再多寫一個 if-else 判斷,同時每個 ViewModel 的依賴也不相同,讓 ViewModelFactory
的 constructor 會需要注入越來越多依賴。
如果改成 Multibindings
,那麼上述的職責就可以讓 Dagger 幫我們處理了。
整理的思路是將 Map
的 Key 定為 ViewModel 的 Class,而 Value 則是 ViewModel 實體,接著將這個 Map
交給 ViewModelFactory
讓他可以根據這些資料產出我們需要的 ViewModel。
首先建立一個 annotation --- ViewModelKey
:
@Target(
AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
這裏用 @MapKey
標註 ViewModelKey
是 Multibindings
的 annotation,同時其 value 的型別是繼承 ViewModel 的 Class。
接著建立一個讓 Dagger 提供 ViewModelFactory 依賴的 Module:
@Module
internal abstract class ViewModelBuilder {
@Binds
internal abstract fun bindViewModelFactory(
factory: TodoViewModelFactory
): ViewModelProvider.Factory
}
這裡我用到了 Binds,現在暫時把他當作是 Provides
的簡化,如果想要知道更詳細的差別可以到官網查詢。
再來我們建立 TasksModule
負責管理 presentation 層關於 Task 的依賴:
@Module
abstract class TasksModule {
@ContributesAndroidInjector(modules = [
ViewModelBuilder::class
])
internal abstract fun tasksFragment(): TasksFragment
@Binds
@IntoMap
@ViewModelKey(TasksViewModel::class)
abstract fun bindViewModel(viewmodel: TasksViewModel): ViewModel
}
我們透過 Dagger 的 Binds
方法讓他提供 TasksViewModel
, @IntoMap
產生了一組 Map<K, Provider<V>>
,這裏 Map
的 key 就是 ViewModelKey 裡的 ViewModel Class,而 value 則是 Binds
提供的 TasksViewModel
。
最後再看看改造後的 TodoViewModelFactory
:
class TodoViewModelFactory @Inject constructor(
private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("Unknown model class: $modelClass")
}
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
現在 TodoViewModelFactory
傳入的是 Multibindings
的 Map,而 Map 的 value 是一個封裝的 Provider
, Provider
可以讓我們在呼叫 Provider.get()
才對依賴實體化,這樣才可以達到每次 View 層在 create 時,獲取的 ViewModel 是全新的。
而 TodoViewModelFactory
的實際作法則是一開始先透過 Map
獲得 modelClass
;如果找不到則用 for-loop 找找看有沒有 modelClass
的子類。
假如找到的話就呼叫 Provider.get()
以獲得新的 ViewModel 並回傳。
最後把 TasksModule 放入 ApplicationComponent 即可。
到這裡 Dagger 的設置已經完成大部分了,明天我會繼續為專案的 Dagger 進行架構上的調整。