前言
※AndroidのdevelopersサイトからUI architecture
読んで、気になる部分をメモする感じです。
今回はSide-effects
を拝見してます。
https://developer.android.com/jetpack/compose/side-effects
キーワード
-
side-effect
:composable functionのスコープ外で発生する状態の変化
メモ
Side-effects in Compose
予測できないrecompositionsがあるため、基本はside-effectがない方が理想です。
State and effect use cases
アプリの状態を変更が必要の場合はEffect Apiを使用、そうすると予想できる副作用に収めます。
また、こちらのAPIを使う場合はその処理がUI関連、unidirectional data flowを崩さないことを意識すること。
LaunchedEffect
composableの中に安全的にsuspend functionsを呼び出す方法。
recompositionの場合はLaunchedEffect
のキーの値が変わったら、前回のcoroutineはキャンセルされ、新しいcoroutineで実行されます。
LaunchedEffect(snackbarHostState) {
// showSnackbarはsuspend funですので、直接使えません
snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
rememberCoroutineScope
LaunchedEffectはcomposableの中しか使えないため、代わりにrememberCoroutineScope
が使えます。
val scope = rememberCoroutineScope()
Button(
onClick = {
// ここのscopeはcomposableではないので、LaunchedEffectが使えない
scope.launch {
snackbarHostState.showSnackbar("Something happened!")
}
}
) { Text("Press me") }
rememberUpdatedState
LaunchedEffect
はkeyが変更するとキャンセルされ、再度実行されます。
もし時間が掛かる処理の後に実行したいメソットがある場合は、途中でkeyが変わると重い処理が再度発生する。この場合はこちらのeffectが使える、recompositionが発生しても都度値を更新します。
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
// delayの間ても常に最新の値が更新される
currentOnTimeout()
}
ソースコードを見ると、意外と難しいことしてませんでした。
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
DisposableEffect
スコープの中にOnDispose
が提供されてる。Compositionから退場する場合はこちらのメソットが呼ばれます。
クリーンアップが必要な処理には向いてます。また、onDisposeの中身が空の場合は見直すべきと思います。
onDispose {
// ここでobserverを解除する
lifecycleOwner.lifecycle.removeObserver(observer)
}
SideEffect
composeの状態をcomposeではないオブジェクトに共有するeffects。こちらはrecompositionする度に実行します。
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
実際SideEffectを使わなくても実現できるではないでしょうかと思っちゃいました。それについてはこちらの記事が参考できます。https://jetc.dev/slack/2021-09-04-why-need-sideffect.html
Compositionは協議である、協議が成功するする前に観測できるside effectが存在すべきではない。そこで、SideEffect
が担保できます。Composistionが成功しないとSideEffect
は実行しません。
個人的なイメージは、自分が作ったComposableと直接な関係がなくて、実装的に実行したい。この場合はSideEffect
を使って、Composableに影響がないスコープで実行します。
produceState
こちらは名前通りStateを生産できます。中身はLaunchedEffectですので、coroutineスコープになります。そして最後に返却される値はStateに包まれます。
また、同期のソースコードも対応て、その場合はawaitDispose
でサブスクリプションを解除できます。
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
return <Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// suspend funが使えます
val image = imageRepository.load(url)
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
derivedStateOf
こちらは状態が他の状態から計算して導いた時に使えます。recompositionが発生しても計算は行いません、指定した状態の変更のみ再計算されます。
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
// recompositionされる度ではなく、todoTasksやhighPriorityKeywordsが変更される時のみ
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf {
todoTasks.filter { task ->
highPriorityKeywords.any { keyword ->
task.contains(keyword)
}
}
}
}
}
snapshotFlow
State<T>
をcold Flowに変換する。
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
Restarting effects
LaunchedEffect
, produceState
, DisposableEffect
は複数のキーを設定できます。キーが変化するとeffectが再度実行されます。
effectsのスコープ内で使われる値はキーとして渡すべき、そうでなければrememberUpdataedState
を使います。
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
currentOnStart
, currentOnStop
は変更しないため、rememberUpdatedState
を使います。
lifecycleOwnerを渡さないとrecomposition後、間違ったlifecycleOwnerが使われるため、問題になります。
Constants as keys
定数をキーとして使うと、前回話したlifecycleに沿うことができます。
例えばLaunchedEffect
とか、ですが使う前にもう本当に必要かどうかを考えましょう。
終わり
思ったより深くて、難しいでした。
もし理解が間違ってた部分がありましたら、ぜひ教えてください!
一緒に成長しましょう😌