今天我想來完成主要的 UseCase ,並順便跟 ViewModel 串接起來。
自從 Bob 大叔提出 Clean Architecture 後到了最近, Android 也為了可以更加容易處理 Domain layer 的部分而開始向大家提出 UseCase
的做法,與其類似的名稱還有 Interactor
等等,但都是描述同一件事,即旨在:
處理實體之間的資料流,並指揮實體達到使用案例 (UseCase) 的目標或是功能。
他可以當成是平常所稱呼的 『邏輯層』,但我們會把邏輯在拆分成一個個的使用案例 (UseCase) 並加以實現。
下面我們先來完成一個較簡單的 UseCase。
Todo App 的主頁大致上有許多比較重要的功能,但是如果依照與實體資料交流的角度來看,我們可以歸納出以下四個使用案例:
先從較簡單的 CompleteTaskUseCase
開始吧,那就直接上程式碼:
class CompleteTaskUseCase @Inject constructor(
private val tasksRepository: TasksRepository
) {
suspend operator fun invoke(task: Task) {
tasksRepository.completeTask(task)
}
}
class TasksViewModel @Inject constructor(
private val completeTaskUseCase: CompleteTaskUseCase
) : ViewModel() {
......
fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {
if (completed) {
completeTaskUseCase(task)
showSnackbarMessage("Task Marked Complete")
}
}
}
完成!這時可能有些人認為這樣寫有什麼優點,直接在 completeTask()
方法內呼叫 tasksRepository.completeTask(task)
,不是更加簡單嗎?
其實沒有錯,但現在讓我們先來看看這樣寫之後會有什麼影響。
從 ViewModel 的角度來看,如果想要完成 “將某個工作事項的狀態切換成已完成” 這個功能的話,他只需要呼叫 completeTask()
並把實際功能交給 UseCase 完成,自己則只需要專注在切換畫面的狀態等工作就好。這麼寫個人覺得有幾個好處:
透過 UseCase ,我們可以將資料流的處理,或是某些邏輯的實現從 ViewModel 裡拆出去。
接下來看看 UseCase 處理較複雜情況時的寫法,但在這之前,我想先來為 UseCase 設計一個架構,讓他可以一次只針對一件事情處理資料。
abstract class UseCase<in P, R>(private val ioDispatcher: CoroutineDispatcher) {
suspend operator fun invoke(parameters: P): Result<R> {
return withContext(ioDispatcher) {
return@withContext executeNow(parameters)
}
}
/**
* 同步執行程式,回傳 [Result]
*/
suspend fun executeNow(parameters: P): Result<R> {
return try {
execute(parameters)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* override 此方法並把邏輯寫在這裡
*/
@Throws(RuntimeException::class)
protected abstract suspend fun execute(parameters: P): Result<R>
}
suspend operator fun <R> UseCase<Unit, R>.invoke(): Result<R> = this(Unit)
再來改造一下 CompleteTaskUseCase
:
class CompleteTaskUseCase @Inject constructor(
private val tasksRepository: ITasksRepository,
ioDispatcher: CoroutineDispatcher
) : UseCase<CompleteTaskUseCase.Params, Unit>(ioDispatcher) {
override suspend fun execute(parameters: CompleteTaskUseCase.Params): Result<Unit> {
wrapEspressoIdlingResource {
return Success(tasksRepository.completeTask(parameters.task))
}
}
data class Params(val task: Task)
}
......
fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {
if (completed) {
completeTaskUseCase(CompleteTaskUseCase.Params(task))
showSnackbarMessage("Task Marked Complete")
}
}
我設計了一個簡單的父類 UseCase
來限制其子類,透過限制回傳的資料型態來達到限制其處理的邏輯範圍。
有了 UseCase
後,就可以來看看比較複雜的 GetTasksUseCase
了。
我們來完成以下功能:
獲得工作項目的資料,可以依據條件獲得不同類型的工作項目
可以根據 filterALL_TASKS
、ACTIVE_TASKS
、COMPLETED_TASKS
,獲得全部、未完成、已完成的工作事項
先看看不使用 UseCase 的寫法:
class TasksViewModel @Inject constructor(
private val tasksRepository: TasksRepository
) : ViewModel() {
......
fun loadTasks(forceUpdate: Boolean) {
// 這是一個 LiveData ,描述 loading 狀態
_dataLoading.value = true
viewModelScope.launch {
val tasksResult = tasksRepository.getTasks(forceUpdate)
if (tasksResult is Success) {
val tasks = tasksResult.data
val tasksToShow = ArrayList<Task>()
// 將取得 Task 的方式用三種 Type 約束
for (task in tasks) {
when (_currentFiltering) {
TasksFilterType.ALL_TASKS -> tasksToShow.add(task)
TasksFilterType.ACTIVE_TASKS -> if (task.isActive) {
tasksToShow.add(task)
}
TasksFilterType.COMPLETED_TASKS -> if (task.isCompleted) {
tasksToShow.add(task)
}
}
}
// 資料獲取成功,將顯示 error 的 LiveData 狀態切成 false
isDataLoadingError.value = false
// 將保存資料的 LiveData 發射出去
_items.value = ArrayList(tasksToShow)
} else {
isDataLoadingError.value = false
_items.value = emptyList()
// 顯示讀去資料錯誤的 Snackbar
showSnackbarMessage(R.string.loading_tasks_error)
}
_dataLoading.value = false
}
}
}
事實上 ViewModel 並不需要知道 Task 資料是如何獲取並加以處理的,對他而言他只需要 loadData
並顯示相應的畫面跟資料就好了,所以可以對這裡再加以改造:
class GetTasksUseCase @Inject constructor(
private val tasksRepository: ITasksRepository,
ioDispatcher: CoroutineDispatcher
) : UseCase<GetTasksUseCase.Params, List<Task>>(ioDispatcher) {
override suspend fun execute(parameters: GetTasksUseCase.Params): Result<List<Task>> {
val tasksResult = tasksRepository.getTasks(parameters.forceUpdate)
// Filter tasks
if (tasksResult is Success && parameters.currentFiltering != ALL_TASKS) {
val tasks = tasksResult.data
val tasksToShow = mutableListOf<Task>()
for (task in tasks) {
when (parameters.currentFiltering) {
ACTIVE_TASKS -> if (task.isActive) {
tasksToShow.add(task)
}
COMPLETED_TASKS -> if (task.isCompleted) {
tasksToShow.add(task)
}
else -> NotImplementedError()
}
}
return Success(tasksToShow)
}
return tasksResult
}
data class Params(
val forceUpdate: Boolean,
val currentFiltering: TasksFilterType = ALL_TASKS
)
}
class TasksViewModel @Inject constructor(
private val getTasksUseCase: GetTasksUseCase,
private val completeTaskUseCase: CompleteTaskUseCase
) : ViewModel() {
......
fun loadTasks(forceUpdate: Boolean) {
_dataLoading.value = true
viewModelScope.launch {
val tasksResult =
getTasksUseCase(GetTasksUseCase.Params(forceUpdate, _currentFiltering))
if (tasksResult is Success) {
isDataLoadingError.value = false
Timber.e("loadTasks: ${tasksResult.data}")
_items.value = tasksResult.data
} else {
isDataLoadingError.value = false
_items.value = emptyList()
showSnackbarMessage(R.string.loading_tasks_error)
}
_dataLoading.value = false
}
}
}
現在 ViewModel 如一開始想要的只需要委託 UseCase 處理資料,自己再顯示對應畫面跟資料就好了,UseCase 也幫助我們順利讓 ViewModel 與資料層解偶,並讓實際的邏輯更加單純。
今天大致示範了如何整理 Domain layer 並利用 UseCase 加以實現,明天就可以利用這些資料完成 Data Bindind 。