LiveData-ktxでコルーチンのキャンセル処理を自動化しよう

2019.07.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Lifecycle-aware components + Kotiln Coroutines

コルーチンを使う際に必要なキャンセル処理を、Lifecycle-aware componentsの一部であるLiveData-KTXを使って実装します。明示的にキャンセル処理を実装する必要が無くなります。

準備

dependencies {
    def lifecycle_version = "2.2.0-alpha02"
    ...
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

UseCase

UseCaseクラスはビジネスロジックの実装をします。

abstract class UseCase<in P, R> {

    private var currentLiveData: LiveData<State<R>>? = null

    operator fun invoke(parameters: P, mediator: MediatorLiveData<State<R>>) {
        currentLiveData?.let { mediator.removeSource(it) }

        // ktx
        val ld = liveData {
            emit(State.Loading)
            try {
                emit(State.Success(withContext(Dispatchers.IO) { execute(parameters) }))
            } catch (e: Exception) {
                Timber.w(e)
                emit(State.Failure(e))
            }
        }
        mediator.addSource(ld) {
            mediator.value = it
        }
        currentLiveData = ld
    }

    abstract suspend fun execute(parameters: P): R
}

class GetTodoListUseCase(
        private val todoRepository: TodoRepository
) : UseCase<Unit, List<Todo>>() {
    override suspend fun execute(parameters: Unit): List<Todo> {
        // ビジネスロジックを記載する(ex. 取得したリストの1~3番目だけ加工する...etc)
        return todoRepository.getNotDoneTodoList()
    }
}

liveData {}が肝となる箇所です。

これで生成するLiveDataはinactive時にキャンセル処理を行ってくれます。また、値の通知にはemit(value)を利用します。※Stateはローディング中・処理完了・処理失敗の状態管理クラスです。

また、UseCaseは再利用(parametersを変えて再リクエスト)可能にしたいので、リクエスト時にMediatorLiveDataを渡し、先程生成したLiveDataを追加しています。(既にリクエストがあった場合は破棄してから追加)

ViewModel

続いてViewModelの実装です。受け取ったリストをLiveDataにセットします。(表示はViewに任せましょう)

class TodoListViewModel(
        getTodoListUseCase: GetTodoListUseCase
) : ViewModel() {

    private val _todoList = MediatorLiveData<State<List<Todo>>>()

    val infoType = MediatorLiveData<InfoType>()
    val todoList = MediatorLiveData<List<Todo>>()

    init {
        infoType.addSource(_todoList) { state ->
            infoType.value = when (state) {
                is State.Loading -> InfoType.Progress
                else -> InfoType.Transparent
            }
        }
        todoList.addSource(_todoList) { state ->
            (state as? State.Success)?.result?.let { todoList.value = it }
        }

        getTodoListUseCase(Unit, _todoList)
    }
}

_todoListはUseCaseの実行結果を受け取るためのLiveDataです。

todoListは表示用のLiveDataですが、_todoListの変更を監視したいので、MediatorLiveDataとしています。

これまではViewModel.onCleared()にて、UseCase等のコルーチンをキャンセルする必要がありましたが、LiveDataが管理しているのでViewModelでの記載は不要となります。

おわりに

ViewModelにて、liveData {} を利用して直接データ操作する手法も散見しますが、UseCaseに分けることでボイラープレートが減ると思います。お試しください。

※筆者はViewModelでemitしたり、showLoading()/hideLoading()したくない派です