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

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

ライフサイクル対応コンポーネントは、アプリ内の論理スコープ用コルーチンを万全の体制でサポートしています。このドキュメントでは、ライフサイクル対応コンポーネントでコルーチンを効果的に使用する方法について説明します。

依存関係を追加する

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

  • Compose の ViewModel ユーティリティには、implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version") を使用します。
  • Compose のライフサイクル ユーティリティには、implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version") を使用します。

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

Compose ライブラリと Lifecycle ライブラリには、アプリで使用できる次の組み込みスコープが用意されています。

ViewModelScope

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

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

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

より高度なユースケースでは、カスタムの CoroutineScope を ViewModel のコンストラクタに直接渡して、デフォルトの viewModelScope を置き換えることができます。このアプローチでは、特に次の点で管理性と柔軟性が向上します。

  • テスト: TestScope を挿入できるため、単体テストで時間を制御し、コルーチンの動作を検証しやすくなります。

  • カスタム構成: ViewModel が処理を開始する前に、特定の CoroutineDispatcher(重い計算用の Dispatchers.Default など)またはカスタム CoroutineExceptionHandler を使用してスコープを構成できます。

コンポジション バインド スコープ

アニメーション、ネットワーク呼び出し、タイマーなどの副作用は、コンポーザブルのライフサイクルにスコープする必要があります。このようにすると、コンポーザブルが画面から離れる(コンポジションを終了する)ときに、実行中のコルーチンが自動的にキャンセルされ、メモリリークを防ぐことができます。

Compose は、コンポジションのスコープを宣言的に処理するための LaunchedEffect API を提供します。

LaunchedEffect は、一時停止関数を実行できる CoroutineScope を作成します。スコープは、ホスト アクティビティのライフサイクルではなく、コンポーザブルのコンポジション ライフサイクルに結び付けられます。

  • Enter: コンポーザブルがコンポジションに入ると、コルーチンが開始されます。
  • 終了: コンポーザブルがコンポジションを離れると、コルーチンはキャンセルされます。
  • 再起動: LaunchedEffect に渡されたキーが変更されると、既存のコルーチンがキャンセルされ、新しいコルーチンが起動されます。

次の例は、LaunchedEffect を使用してパルス アニメーションを作成する方法を示しています。コルーチンはコンポジション内のコンポーザブルの存在に結び付けられ、構成の変更に対応します。

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableLongStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

LaunchedEffect の詳細については、Compose における副作用をご覧ください。

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

Jetpack Compose でフローを安全に収集するには、collectAsStateWithLifecycle API を使用します。この単一の関数は、Flow を Compose State オブジェクトに変換し、ライフサイクル サブスクリプションを自動的に管理します。デフォルトでは、ライフサイクルが STARTED のときに収集が開始され、ライフサイクルが STOPPED のときに収集が停止します。このデフォルトの動作をオーバーライドするには、必要なライフサイクル メソッド(Lifecycle.State.RESUMED など)を指定して minActiveState パラメータを渡します。

次の例は、コンポーザブルで ViewModel の StateFlow を収集する方法を示しています。

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

複数のフローの並列コレクション

Compose では、複数の状態変数を宣言することで、複数のフローを並行して収集できます。collectAsStateWithLifecycle は独自の基盤となるスコープを管理するため、並列コレクションは自動的に処理されます。

@Composable
fun DashboardScreen(viewModel: DashboardViewModel = viewModel()) {
    // Both flows are collected safely in parallel and will emit updates when either changes, the composables will recompose
    val userData by viewModel.userFlow.collectAsStateWithLifecycle()
    val feedData by viewModel.feedFlow.collectAsStateWithLifecycle()

    // ...
}

Flow を使用して値を非同期で計算する

値を非同期で計算する必要がある場合は、stateIn 演算子とともに StateFlow を使用します。

次のスニペットでは、StateFlow に変換された標準の Flow を使用しています。WhileSubscribed(5000) パラメータは、UI が消えてから 5 秒間サブスクリプションをアクティブな状態に保ち、構成の変更を処理します。

val uiState: StateFlow<Result> = flow {
    emit(repository.fetchData())
}
.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = Result.Loading
)

collectAsStateWithLifecycle を使用して、収集した値を Compose の State に変換します。これにより、データが変更されるたびに UI をリアクティブに更新できます。

状態について詳しくは、状態と Jetpack Compose をご覧ください。

参考情報

コンテンツの閲覧

サンプル