iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 15

[Day 15] Dagger 2:Part 3 Complete Dagger

  • 分享至 

  • xImage
  •  

今天要先來將專案全面替換成 Dagger ,現在可以先把 Data layer 及 部分 Presentation layer 的 class 換掉。

Data Layer

先來看看 TasksRepository

class TasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ITasksRepository {
    ......
}

可以看到 TasksRepository 主要依賴了 TasksLocalDataSourceTasksRemoteDataSource

因此如果要讓 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 了。

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 {
    ......
}

Presentation Layer

ViewModel

前幾天有提到 ViewModelFactory ,那麼現在就著手改造吧。

這裏的做法跟前面有一點不同,我們要使用 Dagger 的 Multibindings 來處理, Multibindings 簡單的說就是利用 Map 鍵對的特性達到多個對象綁定到一個集合的功能,讓依賴者可以透過集合得到依賴物而不是經由提供依賴的方法本身。

那麼為什麼要改成這樣呢?

原本的 ViewModelFactory 的寫法,隨著內部的 ViewModel 種類的增加,每此都要再多寫一個 if-else 判斷,同時每個 ViewModel 的依賴也不相同,讓 ViewModelFactory 的 constructor 會需要注入越來越多依賴。

如果改成 Multibindings ,那麼上述的職責就可以讓 Dagger 幫我們處理了。

整理的思路是將 Map 的 Key 定為 ViewModel 的 Class,而 Value 則是 ViewModel 實體,接著將這個 Map 交給 ViewModelFactory 讓他可以根據這些資料產出我們需要的 ViewModel。

Multibindings

首先建立一個 annotation --- ViewModelKey

@Target(
    AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

這裏用 @MapKey 標註 ViewModelKeyMultibindings 的 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 是一個封裝的 ProviderProvider 可以讓我們在呼叫 Provider.get() 才對依賴實體化,這樣才可以達到每次 View 層在 create 時,獲取的 ViewModel 是全新的。

TodoViewModelFactory 的實際作法則是一開始先透過 Map 獲得 modelClass ;如果找不到則用 for-loop 找找看有沒有 modelClass 的子類。

假如找到的話就呼叫 Provider.get() 以獲得新的 ViewModel 並回傳。

最後把 TasksModule 放入 ApplicationComponent 即可。

到這裡 Dagger 的設置已經完成大部分了,明天我會繼續為專案的 Dagger 進行架構上的調整。


上一篇
[Day 14] Dagger 2:Part 2 Basic
下一篇
[Day 16] Dagger 2:Part 4 Refactor
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言