Side-effects in Composeを読む

## 前言

※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で実行されます。

```kotlin
LaunchedEffect(snackbarHostState) {
	// showSnackbarはsuspend funですので、直接使えません
	snackbarHostState.showSnackbar(
		message = "Error message",
		actionLabel = "Retry message"
	)
}
```

### rememberCoroutineScope

LaunchedEffectはcomposableの中しか使えないため、代わりに`rememberCoroutineScope`が使えます。

```kotlin
val scope = rememberCoroutineScope()

Button(
	onClick = {
		// ここのscopeはcomposableではないので、LaunchedEffectが使えない
		scope.launch {
			snackbarHostState.showSnackbar("Something happened!")
		}
	}
) { Text("Press me") }
```

### rememberUpdatedState

`LaunchedEffect`はkeyが変更するとキャンセルされ、再度実行されます。
もし時間が掛かる処理の後に実行したいメソットがある場合は、途中でkeyが変わると重い処理が再度発生する。この場合はこちらのeffectが使える、recompositionが発生しても都度値を更新します。

```kotlin
val currentOnTimeout by rememberUpdatedState(onTimeout)

LaunchedEffect(true) {
		delay(SplashWaitTimeMillis)
		// delayの間ても常に最新の値が更新される
		currentOnTimeout()
}
```

ソースコードを見ると、意外と難しいことしてませんでした。

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

### DisposableEffect

スコープの中に`OnDispose`が提供されてる。Compositionから退場する場合はこちらのメソットが呼ばれます。
クリーンアップが必要な処理には向いてます。また、onDisposeの中身が空の場合は見直すべきと思います。

```kotlin
onDispose {
	// ここでobserverを解除する
	lifecycleOwner.lifecycle.removeObserver(observer)
}
```

### SideEffect

composeの状態をcomposeではないオブジェクトに共有するeffects。こちらはrecompositionする度に実行します。

```kotlin
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`でサブスクリプションを解除できます。

```kotlin
@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が発生しても計算は行いません、指定した状態の変更のみ再計算されます。

```kotlin
@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に変換する。

```kotlin
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}
```

### Restarting effects

`LaunchedEffect`, `produceState`, `DisposableEffect`は複数のキーを設定できます。キーが変化するとeffectが再度実行されます。
effectsのスコープ内で使われる値はキーとして渡すべき、そうでなければ`rememberUpdataedState`を使います。

```kotlin
@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`とか、ですが使う前にもう本当に必要かどうかを考えましょう。

## 終わり

思ったより深くて、難しいでした。
もし理解が間違ってた部分がありましたら、ぜひ教えてください！
一緒に成長しましょう😌