11
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Arsaga App Division 2021🎅🏻Advent Calendar 2021

Day 2

JetpackComposeのEffect系関数一覧

Last updated at Posted at 2021-12-01

はじめに

Google公式ドキュメントで説明されているComposeのEffect系関数について、端的にまとめました。

Effect系関数の役割・目的とは?

Composable関数の実行したときの副作用(Composable関数を実行したことによる、Composable関数外への影響)を定義するための関数。
(Google公式ドキュメント上では「Side-Effect APIs」と表現されている)

LaunchedEffect

「非同期で実行される副作用」を定義する関数。

LaunchedEffect関数にkeyを設定することで、Recomposeに伴うCoroutineの再起動を抑止することができる。

以下の処理の場合、remember関数でsnackbarHostStateが生成されているため、snackbarHostStateはRecomposeを跨いで生存する。つまり、このsnackbarHostStateをkeyとしているLaunchedEffect関数は、Recomposeを跨いで非同期処理を続行する。

LaunchedEffect単体

@Composable
private fun VideoScreen(
    state: PlaybackState,
    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
    if (state is PlaybackState.ERROR) {
        LaunchedEffect(key1 = snackbarHostState) {
            // showSnackbarはsuspend関数
            snackbarHostState.showSnackbar(
                message = "再生に失敗しました",
                actionLabel = "Retry message"
            )
        }
    }
    
    /* 以下、UI構築処理 */
}

LaunchedEffectとrememberUpdatedStateの組み合わせ

Recomposeに伴うLaunchedEffectの再起動は避けたいが、「LaunchedEffectの中で参照しているオブジェクト」は常に最新の値に保ちたいときは、rememberUpdatedState()を利用する。

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // Recomposeの度に最新のラムダ式が代入される
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // key1は固定値のため、Recompseで再起動することはない。
    LaunchedEffect(key1 = true) {
        delay(splashWaitTimeMillis)
        // recomposeで得られた最新のonTimeout関数を実行する
        currentOnTimeout()
    }

    /* 以下その他処理 */
}

rememberUpdatedStateを利用しなくても、引数として定義されている「onTimeout」をそのままLaunchedEffect内で実行すればよいのでは?

No。その場合、LandingScreenのRecomposeが何度実行されても、初回Composeで渡したonTimeoutが実行されてしまう。

DisposableEffect

「同期的に実行される副作用」を定義する関数。
DisposbleEffectでは、onDispose関数を用いることでComposition終了(leaves the Composition)時の挙動も定義できる。

また、LaunchedEffectと同様にkeyを設定することで、Recomposeに伴うDisposableEffectの再起動を抑止することができる。

DisposableEffectとrememberUpdatedStateの組み合わせ

DisposableEffectの再起動は避けたいが、「DisposableEffectの中で参照しているオブジェクト (今回の場合は、onStartとonStop関数オブジェクト)」を常に最新の値に保ちたいときは、rememberUpdatedState()を利用する。

@Composable
fun Screen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, 
    onStop: () -> Unit
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(key1 = lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* 以下その他処理 */
}

rememberUpdatedStateを使わずに、onStartとonStopをそのままobserverの中で実行しても同じ結果になるのでは?

No。その場合、onStartもしくはonStopが更新されてHomeScreenがRecomposeしても、「observerの引数に与えたラムダ式」は永遠に、初回のcomposition時に与えたonStartと onStopを実行し続けてしまう。
lifecycleOwner(Effectのkey)は変更されていないが、onStartもしくはonStopが変更されて、 HomeScreenのReCompositionが起動した場合がこれにあたる。

rememberCoroutineScope

Composable関数内で使用するCoroutineScopeを生成する時に使用。
このScopeはRecompositionを跨いで生存する。

@Composable
fun Screen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            Button(
                onClick = {
                    scope.launch {
                        scaffoldState.snackbarHostState
                            .showSnackbar("")
                    }
                }
            ) {
                Text("スナックバー表示")
            }
        }
    }
}

SideEffect

Compositionの成功後(recomposition含む)に必ず実行したい副作用を定義する。

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember { MockFirebaseAnalytics() }

    // composition成功後に必ず実行される
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }

    return analytics
}

produceState

非同期処理(Coroutine、Rxjava、コールバック関数等)の「実行~値の取得までの流れ」をStateクラスで表現できる。
Composable関数外の状態をCompose内のStateとして取り込む際に利用できる。

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<HiResImage>> {

    return produceState(
        initialValue = Result.Loading<HiResImage>() as Result<HiResImage>,
        url,
        imageRepository
    ) {
        // produceState内で実行するこの関数はCoroutineScope内で実行されるため、suspend関数の実行も可能
        val hiResImage = imageRepository.loadAsync(url).first()
        this.value = if (hiResImage == null) {
            Result.Error("読み込み失敗")
        } else {
            Result.Success(hiResImage)
        }
    }
}

derivedStateOf

あるStateに紐づいた新しいStateを作成するときに使う。
derivedStateOfで定義したラムダ式内で、何らかのStateを参照していた場合、
そのStateが更新される度にderivedStateの値を更新される。

recompositon時のパフォーマンス改善で利用することが多い。
(derivedStateOfを利用するとこで、recompositionのたびに式が実行される事態を防ぐことができる。)

@Composable
fun derivedExample() {
    val rootState: MutableState<String> = remember { mutableStateOf("A") }
    
    /** derivedStateOfに渡したラムダ式はrootStateが更新される度に評価され、
     * rootStateの更新に紐づいてderivedStateの値が更新される。*/
    val derivedState: State<Boolean> = remember() { derivedStateOf() { rootState.value == "A" } }
}
11
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?