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

Kotlin コルーチンは、非同期コードを記述できる API を提供します。Kotlin コルーチンでは、CoroutineScope を定義できます。これは、コルーチンを実行するタイミングを管理するのに役立ちます。各非同期オペレーションは、特定のスコープ内で実行されます。

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

依存関係を追加する

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

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

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

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

ViewModelScope

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

次の例に示すように、ViewModel の viewModelScope プロパティを介して ViewModel の CoroutineScope にアクセスできます。

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 には、Composition のスコープを宣言的に処理するための LaunchedEffect API が用意されています。

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

  • 開始: コンポーザブルがコンポジションに入ると、コルーチンが開始されます。
  • 終了: コンポーザブルがコンポジションから離れると、コルーチンはキャンセルされます。
  • 再起動: 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 をご覧ください。

参考情報

コンテンツを閲覧する

サンプル