🐻‍❄️

Jetpack Compose の rememberUpdatedState の動作を調べる

2022/02/15に公開

rememberUpdatedState とは

rememberUpdatedState がやることは特定の値を保持してコンポーザブルの特定の箇所から参照できるようにするだけです。公式ドキュメントには色々と記載がありますが rememberUpdatedState がやることはシンプルにそれだけです。

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

なぜ rememberUpdatedState を利用しなければならないのか

例えば数秒経過後に一度だけ特定の処理を実行するコンポーザブルを定義したとします。

@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    // LaunchedEffect のキーを true にすると、コンポーザブルの生成後に一度だけ実行できる
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        onTimeout()
    }

    /* Landing screen content */
}

このコンポーザブルは一見問題なく動きそうですが、再コンポーズ時に渡された新しい onTimeout が実行されずに古い onTimeout が実行されるという問題があります。これは Launched Effect が起動するコルーチン内で onTimeoutA を直接参照してしまっているためです。

// 入場時の呼び出し: onTimeout には onTimeoutA を渡す
fun LandingScreen(onTimeout: () -> Unit) {
    // 入場時は LaunchedEffect でコルーチンを起動していないので、コルーチンを起動する
    // 起動するコルーチンは onTimeoutA を直接参照して、数秒後に onTimeoutA を呼び出す
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        onTimeout()
    }

    /* Landing screen content */
}

// 再コンポーズ時の呼び出し: onTimeout には onTimeoutB を渡す
fun LandingScreen(onTimeout: () -> Unit) {
    // 入場時にコルーチンは起動済み、キー値も変更されていないのでコルーチンの再起動はしない。
    // 入場時に起動した onTimeoutA を呼び出すコルーチンが実行したままになり、onTimeoutB を呼び出すコルーチンは実行されない
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        onTimeout()
    }

    /* Landing screen content */
}

この問題を解決するのに rememberUpdatedState を利用します。以下のように rememberUpdatedState を利用してcurrnetOnTimeout を参照する様にコードを変更すると起動中のコルーチンは最新の onTimeout を参照してくれるようになります。

@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    // 再コンポーズされたときに onTimeout が更新される、そのため必ず最新の onTimeout を参照できる
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)

        // 引数で渡された onTimeout を直接参照せず、currentOnTimeout を参照しているので、最新の onTimeout が実行される
        currentOnTimeout()
    }

    /* Landing screen content */
}

rememberUpdatedState を利用しなくても Launched Effect のキーを onTimeout に変えればよいのでは

Launched Effect にコルーチンで参照する値をキーに設定すると古い値を参照しているコルーチンはキャンセルされ新しい値を参照するコルーチンが起動されるので問題は解決できそうです。

ですが今回のように数十秒かかる処理を実行する必要があるときにはコルーチンを再起動をしたくない場合には途中でコルーチンが参照する値を更新できる rememberUpdatedState を利用した実装方法は有効になるかと思います。

Launched Effect のキーを設定することでほとんどの問題は解決できそうなので rememberUpdatedState を利用するのは最終手段になるのかなと思います。

参考文献

Discussion