🐻‍❄️

Jetpack Compose の rememberCoroutineScope の動作を調べる

2022/02/13に公開

rememberCoroutineScope とは

rememberCoroutineScope には以下の特徴があり、Composable 外部からコルーチンを起動するのに使われる。

  • Comosable が入場したときに CoroutineScope が作成され、退場したときに CoroutineScope が破棄される。
  • 作成した CoroutineScope を使って Comopsable 外部からコルーチンを起動できる。
  • 作成した CoroutineScope は退場時に破棄される、そのため起動中のコルーチンは退場時に全部キャンセルされる
@Composable
inline fun rememberCoroutineScope(
    getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope {
    val composer = currentComposer
    val wrapper = remember {
        CompositionScopedCoroutineScopeCanceller(
            createCompositionCoroutineScope(getContext(), composer)
        )
    }
    return wrapper.coroutineScope
}

※ コンポーザブルの入場と退場についてはこちらに記載があるのでわからない方はこちらを参考してください。

lifecycle.png

rememberCoroutineScope の動きを確認してみる

サンプル

以下のサンプルを作成して rememberCoroutineScope の動作を確認してみる。

  • rememberCoroutineScope を利用して CoroutineScope を取得する
  • 取得した CoroutineScope を利用してクリック時にカウンタ値を更新するコルーチンを起動する
  • カウンタ値を更新するコルーチンには開始・キャンセルのタイミングがわかるようにトースト表示を追加している。
@Composable
fun RememberCoroutineScopeSample() {
    val count = remember { mutableStateOf(0f) }
    val scope = rememberCoroutineScope()
    val context = LocalContext.current

    Box(modifier = Modifier.fillMaxSize()) {
        Column(
            modifier = Modifier
                .wrapContentSize()
                .align(Alignment.Center)
        ) {
            Text(
                text = "%.2f".format(count.value),
                style = MaterialTheme.typography.h1,
                maxLines = 1,
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .wrapContentSize()
            )

            Button(
                onClick = { increment(context, scope, count) },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp)
            ) {
                Text(text = "INCREMENT")
            }

            Button(
                onClick = { decrement(context, scope, count) },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp)
            ) {
                Text(text = "DECREMENT")
            }
        }
    }
}

private fun increment(context: Context, scope: CoroutineScope, count: MutableState<Float>) {
    scope.launch {
        Toast.makeText(context, "START INCREMENT", Toast.LENGTH_SHORT).show()
        repeat(10) {
            delay(300)
            count.value = count.value + 0.1f
        }
        Toast.makeText(context, "END INCREMENT", Toast.LENGTH_SHORT).show()
    }
}

private fun decrement(context: Context, scope: CoroutineScope, count: MutableState<Float>) {
    scope.launch {
        Toast.makeText(context, "START DECREMENT", Toast.LENGTH_SHORT).show()
        repeat(10) {
            delay(300)
            count.value = count.value - 0.1f
        }
        Toast.makeText(context, "END DECREMENT", Toast.LENGTH_SHORT).show()
    }
}

動作結果

  • rememberCoroutineScope で CoroutineScope が生成される
  • INCREMENT をクリックすると CoroutineScope を利用してコルーチンを起動し数値が増加していく。
  • DECREMENT をクリックすると CoroutineScope を利用してコルーチンを起動し数値が減少していく。
  • もしコルーチン実行中に前の画面に戻り Composable が退場した場合にはコルーチンがキャンセルされる。

Device-2022_02_13_16_34_26.gif

rememberCoroutineScope の何が嬉しいのか?

似たようなもので Launched Effect がありますが Composable 関数からしかコルーチンを起動でまませでっすが前述の通り rememberCoroutineScope を利用すると非 Composable 関数からコルーチンを起動できるようになります。

非 Composable 関数で呼び出せると何が良いことがあるかと考えるかと思うのですが、非 Composable 関数で呼び出せるようになるとクリックイベントをトリガーにしてアニメーションや定期的な値更新をコルーチンで記述できるようになります。

  • Button の onClick が呼び出されたら、数値を定期的に更新するコルーチンを起動して、表示を更新する
  • Button の onClick が呼び出されたら、透過度を定期的に更新するコルーチンを起動して、フェードアウト表示する

またアニメーションや定期的な値更新を実行するコルーチンは Composable の退場時にあわせてキャンセルされるので、今までの Android View では必要だったアニメーションのキャンセル処理なども必要ないのでかなり便利です。

ViewModel 経由で呼び出すのと何が違うのか?

MVVM でアプリを作っている場合だと ViewModel 経由で実行するのと同じではと思ったのですがライフサイクルの違いがあり、それに伴う問題もでてくるはずなのでどのような処理をどっちでやるか使い分けが必要そうです。

  • もし MVVM でアプリを作っているのであれば ViewModel も CoroutineScope を持っているはずなので同じことが実行できるはず。
  • ViewModel は各画面ごとに定義されることが多い、そのためCoroutineScope のライフサイクルも画面の状態に紐づくことになる。
  • もし画面がバックスタックに積まれたときに ViewModel が破棄されない作りになっている場合には起動中のコルーチンが動きっぱなしになる可能性がありそう。
  • Jetpack Compose に限らず Android View を利用した開発でもそうですがアニメーションの制御など View 内で完結する処理は rememberCoroutineScope を利用して、ViewModel でデータ取得やデータ変換が絡むものは ViewModel を経由させるというような使い分けが必要そう。

参考文献

Discussion