5
4

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.

Jetpack Compose副作用 関数整理

Last updated at Posted at 2023-01-19

副作用

副作用(プログラム)Wikipedia

プログラミングにおいて、式の評価による作用には、主たる作用とそれ以外の副作用(side effect)とがある。 式は、評価値を得ること(※関数では「引数を受け取り値を返す」と表現する)が主たる作用とされ、それ以外のコンピュータの論理的状態(ローカル環境以外の状態変数の値)を変化させる作用を副作用という。

fun sideEffect(){
    anyGlobalValue++  // 副作用
}

fun getValue(): Int {
    return anyGlobalValue
}

こういうの。
getValueをいつ呼び出しても同じ値が返ってくる状態にしておくのが副作用がないプログラム。いつ呼び出しても同じ値が返ってくる=>参照透過ともいう。

Compose における副作用

副作用とは、コンポーズ可能な関数の範囲外で発生するアプリの状態の変化を指します。

話をコンポーズとそれ以外に限定しているが、Wikipedia様の言ってることと同じ。

状態と Jetpack Compose

アプリにおいて状態とは、時間とともに変化する可能性がある値すべてを指します。これは非常に広範な定義であり、Room データベースにも、クラス内の変数一つにも当てはまります。

  • データベースの値たち
  • アニメーション
  • ユーザ操作外で表示されるUI(ネットワーク失敗時に表示するスナックバーなど)
  • State()

状態は常にそこかしこにある不安定なモノたち。参照透過でない(= 呼び出すタイミングで中身が変わっている)モノたち。

ざっくりこの状態を変更すること副作用といい、副作用を操作する関数がJetpackComposeには用意されている。

LaunchedEffect

コンポーザブルのスコープ内で suspend 関数を実行する

@Composable
fun LaunchedEffect(key1: Any?, key2: Any?, key3: Any?, block: suspend CoroutineScope.() -> Unit): Unit

初回コンポジションで実行され、以降はコンポジションが退場するまで、key変更を監視して変更があるとblockを発火する。

使い所
// なんらかのフラグがたったらUIを更新する
LaunchedEffect(isOffline) {
    if (isOffline) snackbarHostState.showSnackbar(
        message = notConnected,
        duration = Indefinite
    )
}

rememberCoroutineScope

コンポーザブルの外部でコルーチンを起動するためのスコープ

@Composable
inline fun rememberCoroutineScope(
    crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope

中身はCoroutineScope。
このScopeを使用しているComposableがコンポジションから退場すると、自動的に削除される。
LaunchedEffectと違い、Coroutineのライフサイクルを制御しやすいのが特徴。

使い所
val scope = rememberCoroutineScope()
Button(
    onClick = {
        scope.launch {
            scaffoldState.snackbarHostState.showSnackbar("Something happened!")
            }
        }
    )
LaunchedEffect(anyKey) { scope.cancel() } // Coroutineのライフサイクルをいじれる利点

// Scopeを共有できる利点
@Composable fun A(scope) {B(scope) }
@Composable fun B(scope) {C(scope) }
@Composable fun C(scope) { ..Any }

DisposableEffect

クリーンアップが必要な作用

@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) 

LaunchedEffectに退場時の処理を追加する余地(onDispose)をくわえたもの

使い所

リソース解放や解除に使える

DisposableEffect(key) {
    // コンポジション入場時の処理
    register()
    onDispose {
        // コンポジション退場時の処理
        unregister()
    }
}

rememberUpdatedState

値が変化しても再起動すべきでない作用の値を参照する

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

内部的には値をrememberしているのみ
もっぱら関数をStateでラップして、その関数が評価される時にその時点の最新の関数処理を行うためのもの

使い所
LaunchedEffect(Unit)やDisposableEffect(Unit) 等、コンポジション中に一度だけ起こしたい処理の内部で使用される値が常に最新のものであるべき状況 って時に使用する。

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

    // この1行でonTimeoutを参照として保持できる。
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    LaunchedEffect(Unit) {
        // delay中にonTimeoutの処理が変化しても常に最新のonTimeoutを発火できる。
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }
}

LaunchedEffect(Unit)を使用するときは、同時にrememberUpdatedStateで包むべき値が内部にないか確認するとよい。

SideEffect

Compose の状態を非 Compose コードに公開する

fun SideEffect(effect: () -> Unit): Unit

現在のCompositionが終了した後effect内の処理を発火する

使い所
// Composeの状態変更をライブラリに通知する時に使用できる。
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState

Compose外部のデータをCompose内で使用するStateに変換できる。

@Composable
fun <T> produceState(
    initialValue: T,
    vararg keys: Any?,
    @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
    LaunchedEffect(keys = keys) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

keyに監視したい値を突っ込むと、変更を監視して常に最新のStateを返してくれる。

使い所
参考:

@Composable
internal fun <T> Flow<T>.collectAsStateWithLifecycle(
    initialValue: T,
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    context: CoroutineContext = EmptyCoroutineContext
): State<T> {
    return produceState(initialValue, this, lifecycle, minActiveState, context) {
        lifecycle.repeatOnLifecycle(minActiveState) {
            if (context == EmptyCoroutineContext) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            } else withContext(context) {
                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
            }
        }
    }
}
/*
特定のComposeの生存時間を測るってことが以下でできる
*/
val time by produceState(initialValue = 0f) {
    val screenDisplayed = SystemClock.uptimeMillis()
    while (true) {
        delay(500)
        value =  ((SystemClock.uptimeMillis() - screenDisplayed)/1000).toFloat()
    }
}

Text(text = time.toInt().toString())

derivedStateOf

不要な再コンポーズに巻き込まれないStateを、別のStateから作り出す。

fun <T : Any?> derivedStateOf(calculation: () -> T): State<T>
@Composable fun Example() {
    var a by remember { mutableStateOf(0) }
    var b by remember { mutableStateOf(0) }
    var c by remember { mutableStateOf(0) }
    val sum = remember { derivedStateOf { a + b } }

    CountDisplay(sum) // derivedStateOfで包むと、a or bが変化した時のみここの再コンポーズが走ることを保証できる。
    CountDisplay(c) 
}

使い所
厳密に使っている値が変化した時のみ再コンポーズを走らせたい時に使用する

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // filterは重いので、todotasksの変更がないときの再コンポーズから保護する
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
    }
}

snapshotFlow

Compose の状態をFlowに変換する。

fun <T : Any?> snapshotFlow(block: () -> T): Flow<T>

内部的にはblock評価によって得られる値をStateで包んでemitしFlowを返している。

サンプル
// rememberしているなんらかのStateのパラメータをFlowに変換し、イベントを発火できる。
// rememberLazyListStateのような、JetpackComposeAPIから得られるStateから、イベントを発火させたい時に使う。
lazyListState = rememberLazyListState()
snapshotFlow { lazyListState.firstVisibleItemIndex }.collect {firstVisibleItemIndex ->  
    when(firstVisibleItemIndex) {
        0 -> viewModel.zeroUiAnimEvent()
        10 -> viewModel.tenUiAnimEvent()
    }
}
5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?