はじめに
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" } }
}