ライフサイクル対応コンポーネントで Kotlin コルーチンを使用する

Kotlin のコルーチンには、非同期コードを記述できる API が用意されています。Kotlin コルーチンを使用すると、CoroutineScope を定義して、コルーチンを実行するタイミングを管理できます。非同期操作はそれぞれ、特定のスコープで実行されます。

ライフサイクル対応コンポーネントは、アプリ内の論理スコープ用コルーチンと、LiveData との相互運用レイヤを万全の体制でサポートしています。このトピックでは、ライフサイクル対応コンポーネントでコルーチンを効果的に使用する方法について説明します。

KTX 依存関係を追加する

このトピックで説明する組み込みのコルーチン スコープは、対応する各コンポーネントの KTX 拡張機能に含まれています。これらのスコープを使用するときは、必ず適切な依存関係を追加してください。

  • ViewModelScope には、androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 以降を使用します。
  • LifecycleScope には、androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 以降を使用します。
  • liveData には、androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 以降を使用します。

ライフサイクル対応コルーチン スコープ

ライフサイクル対応コンポーネントでは、アプリで使用できる次の組み込みスコープが定義されます。

ViewModelScope

ViewModelScope は、アプリで ViewModel ごとに定義されます。このスコープ内で起動されたすべてのコルーチンは、ViewModel が消去されると自動的にキャンセルされます。ViewModel がアクティブな場合にのみ行う必要がある作業があるとき、コルーチンが役に立ちます。たとえばレイアウト用のデータを計算している場合、作業を ViewModel にスコープする必要があるため、ViewModel が消去されると、作業はリソースの消費を避けるために自動的にキャンセルされます。

ViewModelCoroutineScope にアクセスするには、次の例に示すように ViewModel の viewModelScope プロパティを使用します。

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

LifecycleScope

LifecycleScope は、Lifecycle オブジェクトごとに定義されます。このスコープ内で起動されたすべてのコルーチンは、Lifecycle が破棄されたときにキャンセルされます。LifecycleCoroutineScope には、lifecycle.coroutineScope プロパティまたは lifecycleOwner.lifecycleScope プロパティを介してアクセスできます。

以下の例は、lifecycleOwner.lifecycleScope を使用して計算済みテキストを非同期で作成する方法を示しています。

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

再実行可能なライフサイクル対応コルーチン

長時間実行オペレーションは、LifecycleDESTROYED の場合、lifecycleScope により適切な方法で自動的にキャンセルされますが、Lifecycle が特定の状態のときにコードブロックの実行の開始が、また別の状態のときにキャンセルが必要な場合もあります。たとえば、LifecycleSTARTED のときに Flow を収集し、STOPPED になるとコレクションをキャンセルする必要がある場合もあります。この方法では、UI が画面に表示されるときにのみ Flow 出力を処理することで、リソースを節約し、場合によってはアプリのクラッシュを回避します。

このようなケースのために、LifecycleLifecycleOwner にはこれを実現する suspend repeatOnLifecycle API が用意されています。次の例には、関連する Lifecycle が少なくとも STARTED 状態にあるたびに実行し、LifecycleSTOPPED になるとキャンセルするコードブロックが含まれています。

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

ライフサイクル対応フロー収集

ライフサイクル対応収集を単一のフローで行うだけでよい場合は、Flow.flowWithLifecycle() メソッドを使用してコードを簡略化できます。

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

ただし、複数のフローで並行してライフサイクル対応収集を行う必要がある場合は、各フローを別々のコルーチンで収集する必要があります。その場合は、repeatOnLifecycle() を直接使用すると効率的です。

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

ライフサイクル対応コルーチンを停止する

長時間実行オペレーションは、CoroutineScope により適切な方法で自動的にキャンセルされますが、Lifecycle が特定の状態でない限り、コードブロックの実行の停止が必要な場合もあります。たとえば、FragmentTransaction を実行するには、Lifecycle が少なくとも STARTED になるまで待つ必要があります。このような場合、Lifecycle では、lifecycle.whenCreatedlifecycle.whenStartedlifecycle.whenResumed などの追加メソッドを提供します。Lifecycle が必要最小限の状態にない場合、これらのブロック内で実行されているすべてのコルーチンが停止します。

下記の例には、関連する Lifecycle が少なくとも STARTED 状態の場合にのみ実行するコードブロックが含まれています。

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

when メソッドのいずれかを介してコルーチンが有効なときに、Lifecycle が破棄されると、そのコルーチンは自動的にキャンセルされます。下記の例では、Lifecycle の状態が DESTROYED になると、finally ブロックが実行します。

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

LiveData でコルーチンを使用する

LiveData を使用する場合、値の非同期的な計算が必要になることがあります。たとえば、ユーザーの優先設定を取得して UI に提供する場合などです。このような場合、liveData ビルダー関数を使用して suspend 関数を呼び出し、結果を LiveData オブジェクトとして提供できます。

以下の例で、loadUser() は他の場所で宣言された中断関数です。liveData ビルダー関数を使用して loadUser() を非同期で呼び出したら、emit() を使用して結果を出力します。

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

liveData 要素は、コルーチンと LiveData の間で構造化された同時実行プリミティブとして機能します。コードブロックの実行は、LiveData が有効になると開始し、LiveData が無効になったときに設定可能なタイムアウトが経過すると自動的にキャンセルされます。完了前にキャンセルされた場合は、LiveData が再びアクティブになると再開されます。以前の実行で正常に完了した場合は再開されません。再開されるのは、自動的にキャンセルされた場合だけです。他の理由(CancellationException のスローなど)でブロックがキャンセルされた場合は、再開されません

ブロックから複数の値を出力することもできます。メインスレッドで LiveData 値が設定されるまで、各 emit() 呼び出しによりブロックの実行が停止します。

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

また、次の例のように liveDataTransformations を組み合わせることもできます。

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

新しい値を出力する必要がある場合は、emitSource() 関数を呼び出すことで LiveData から複数の値を出力できます。emit() または emitSource() を呼び出すたびに、以前追加したソースが削除されます。

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

コルーチンについて詳しくは、以下のリンク先をご覧ください。

参考情報

ライフサイクル対応コンポーネントでコルーチンを使用する方法について詳しくは、以下のリソースをご覧ください。

サンプル

ブログ