$30 off During Our Annual Pro Sale. View Details »

Jetpack Compose の Side-effect を使いこなす / DroidKaigi 2023

star_zero
September 15, 2023

Jetpack Compose の Side-effect を使いこなす / DroidKaigi 2023

star_zero

September 15, 2023
Tweet

More Decks by star_zero

Other Decks in Programming

Transcript

  1. Jetpack Compose の
    Side-effect を使いこなす
    Kenji Abe - 2023/09/15

    View Slide

  2. 自己紹介
    ● Kenji Abe
    ● Google Developers Expert for Android, Kotlin
    ● DeNA Co., Ltd.
    ● X: @STAR_ZERO
    ● Bluesky: @star-zero.com
    2

    View Slide

  3. Side-effect?
    (副作用)
    3

    View Slide

  4. Side effect (副作用)
    ● 関数や操作などが結果を返すなどの主となる効果以外の効果
    ○ グローバル変数の変更
    ○ I/O操作
    ● ほかのSide effectを起こす関数の呼び出し
    ● 実行順によって結果が変わる可能性がある
    ● デバッグが困難になる
    ● テストが難しくなる
    4

    View Slide

  5. Composeにおける
    Side effect
    5

    View Slide

  6. 副作用とは、
    コンポーズ可能な関数の範囲外で
    発生するアプリの状態の変化を
    指します。
    6
    https://developer.android.com/jetpack/compose/side-effects?hl=ja

    View Slide

  7. ComposeにおけるSide effect
    ● Composable関数はSide effectがないようにすることが理想
    ● 予測できないRecomposition
    ○ 実行順
    ○ スキップ
    ○ 破棄
    7

    View Slide

  8. Side effectが必要な状況
    8

    View Slide

  9. Side effectが必要な状況
    ● スナックバーを表示する
    ● 1 回限りのイベントをトリガーする
    ● 特定の状態で別の画面に移動する
    ● などなど
    9
    https://developer.android.com/jetpack/compose/side-effects?hl=ja

    View Slide

  10. Side effectが必要な状況
    ● スナックバーを表示する
    ● 1 回限りのイベントをトリガーする
    ● 特定の状態で別の画面に移動する
    ● などなど
    10
    https://developer.android.com/jetpack/compose/side-effects?hl=ja
    Side effectを安全に
    実行する必要がある

    View Slide

  11. Side effect APIs
    ● LaunchedEffect
    ● rememberCoroutineScope
    ● DisposableEffect
    ● rememberUpdatedState
    ● SideEffect
    ● derivedStateOf
    ● produceState
    ● snapshotFlow
    11
    https://developer.android.com/jetpack/compose/side-effects?hl=ja

    View Slide

  12. LaunchedEffect
    12

    View Slide

  13. LauchedEffect
    var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }

    View Slide

  14. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect

    View Slide

  15. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    後述

    View Slide

  16. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    Coroutines

    View Slide

  17. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    Snackbarを表示

    View Slide

  18. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    フラグを戻して、次回も表示できるように

    View Slide

  19. LaunchedEffectの
    引数について
    19

    View Slide

  20. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    Key: 値が変わったときに起動される

    View Slide

  21. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false
    1

    View Slide

  22. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false
    1
    初回は必ず起動する
    2

    View Slide

  23. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    Recomposition

    View Slide

  24. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false のまま
    3

    View Slide

  25. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false のまま
    3
    Key が変更されてないので
    実行されない
    4

    View Slide

  26. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    showSnackbar = true

    View Slide

  27. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false → true
    5

    View Slide

  28. var showSnackbar by remember { mutableStateOf(false) }
    LaunchedEffect(showSnackbar) {
    if (showSnackbar) {
    snackbarHostState.showSnackbar(
    message = "OK"
    )
    showSnackbar = false
    }
    }
    LauchedEffect
    false → true
    5
    Key が変更されたので
    実行される
    6

    View Slide

  29. いろいろなKey
    29

    View Slide

  30. LaunchedEffect(Unit) {
    // ...
    }
    LauchedEffect
    ずっと同じ値
    最初の1回だけ実行

    View Slide

  31. LaunchedEffect(key1, key2, key3) {
    // ...
    }
    LauchedEffect
    複数のKey
    どれかが変わったら実行する

    View Slide

  32. LaunchedEffect
    のCoroutines
    32

    View Slide

  33. LaunchedEffectのCoroutines
    ● キャンセルするタイミング
    ○ keyが変わってLauchedEffectが再起動するとき
    ○ CompositionからLeaveするとき
    33
    https://developer.android.com/jetpack/compose/lifecycle

    View Slide

  34. LaunchedEffect(key) {
    delay(5000)
    Timber.d("OK")
    }
    LauchedEffect

    View Slide

  35. LaunchedEffect(key) {
    delay(5000)
    Timber.d("OK")
    }
    LauchedEffect
    5秒経過前にkeyが変わると
    ログが出力されずに最初から数え直し

    View Slide

  36. LaunchedEffect(key) {
    delay(5000)
    Timber.d("OK")
    }
    LauchedEffect
    5秒経過前にCompositionからLeaveすると
    ログが出力されずに終了

    View Slide

  37. rememberCoroutineScope
    37

    View Slide

  38. rememberCoroutineScope
    val scope = rememberCoroutineScope()
    Button(
    onClick = {
    scope.launch {
    snackbarHostState.showSnackbar("Hello, World")
    }
    }
    ) {
    Text(text = "Show Snackbar")
    }

    View Slide

  39. val scope = rememberCoroutineScope()
    Button(
    onClick = {
    scope.launch {
    snackbarHostState.showSnackbar("Hello, World")
    }
    }
    ) {
    Text(text = "Show Snackbar")
    }
    rememberCoroutineScope
    CoroutinesScope取得

    View Slide

  40. val scope = rememberCoroutineScope()
    Button(
    onClick = {
    scope.launch {
    snackbarHostState.showSnackbar("Hello, World")
    }
    }
    ) {
    Text(text = "Show Snackbar")
    }
    rememberCoroutineScope
    ScopeからCoroutines起動

    View Slide

  41. val scope = rememberCoroutineScope()
    Button(
    onClick = {
    scope.launch {
    snackbarHostState.showSnackbar("Hello, World")
    }
    }
    ) {
    Text(text = "Show Snackbar")
    }
    rememberCoroutineScope
    Composition から Leave するときにキャンセル

    View Slide

  42. DisposableEffect
    45

    View Slide

  43. DisposableEffect
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }

    View Slide

  44. val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }
    DisposableEffect
    LifecycleOwner取得

    View Slide

  45. val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }
    DisposableEffect
    引数は LauchedEffect と同じ感じ

    View Slide

  46. val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }
    DisposableEffect
    Coroutinesじゃない

    View Slide

  47. val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }
    DisposableEffect
    Lifecycle監視の処理

    View Slide

  48. val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_START) {
    // ...
    }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
    lifecycleOwner.lifecycle.removeObserver(observer)
    }
    }
    DisposableEffect
    クリーンアップの処理

    View Slide

  49. DisposableEffect
    の処理の流れ
    52

    View Slide

  50. DisposableEffect
    var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }

    View Slide

  51. var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }
    DisposableEffect
    false
    1

    View Slide

  52. var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }
    DisposableEffect
    false
    1
    実行
    2

    View Slide

  53. var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }
    DisposableEffect
    false → true
    3

    View Slide

  54. var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }
    DisposableEffect
    実行
    4

    View Slide

  55. var flag by remember { mutableStateOf(false) }
    DisposableEffect(flag) {
    // ...
    onDispose {
    // ...
    }
    }
    DisposableEffect
    実行
    5

    View Slide

  56. 59
    LaunchedEffect DisposableEffect
    ● クリーンアップできない
    ● Coroutinesで起動
    ○ 若干のオーバーヘッド
    ● onDisposeによるクリーンアップ
    ● Coroutinesではない

    View Slide

  57. rememberUpdatedState
    60

    View Slide

  58. rememberUpdatedState
    を使わなかった場合
    61

    View Slide

  59. var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
    Text(text = "Increment: $count")
    }
    Content(count = count)
    rememberUpdatedState

    View Slide

  60. var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
    Text(text = "Increment: $count")
    }
    Content(count = count)
    rememberUpdatedState
    単純なインクリメント処理

    View Slide

  61. var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
    Text(text = "Increment: $count")
    }
    Content(count = count)
    rememberUpdatedState
    別のComposable関数へ渡す

    View Slide

  62. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState

    View Slide

  63. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    0, 1, 2 … と増えていく

    View Slide

  64. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    最初の1回だけ実行

    View Slide

  65. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    5秒後にログを表示

    View Slide

  66. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    ???

    View Slide

  67. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    Count = 0

    View Slide

  68. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    Count = 0
    最初の引数がキャプチャされるので 0 のまま

    View Slide

  69. 値が変わったら LauchedEffect
    を再起動する
    72

    View Slide

  70. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(count) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState

    View Slide

  71. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(count) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    値が変わったら再起動

    View Slide

  72. @Composable
    private fun Content(count: Int) {
    LaunchedEffect(count) {
    delay(5000)
    Timber.d("Count = $count")
    }
    }
    rememberUpdatedState
    値が変わるたびに最初から数え直し

    View Slide

  73. rememberUpdatedState
    を使う
    76

    View Slide

  74. @Composable
    private fun Content(count: Int) {
    val currentCount by rememberUpdatedState(count)
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $currentCount")
    }
    }
    rememberUpdatedState

    View Slide

  75. @Composable
    private fun Content(count: Int) {
    val currentCount by rememberUpdatedState(count)
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $currentCount")
    }
    }
    rememberUpdatedState
    引数の値を rememberUpdatedState で Stateに変換

    View Slide

  76. @Composable
    private fun Content(count: Int) {
    val currentCount by rememberUpdatedState(count)
    LaunchedEffect(Unit) {
    delay(5000)
    Timber.d("Count = $currentCount")
    }
    }
    rememberUpdatedState
    変換した変数を参照

    View Slide

  77. val currentCount = rememberUpdatedState(count)
    val currentCount = remember { mutableIntStateOf(count) }
    currentCount.value = count
    rememberUpdatedState

    View Slide

  78. rememberUpdatedState
    が必要かどうかの判断
    81

    View Slide

  79. rememberUpdatedStateが必要か検討するポイント
    ● Composeの外で変化する状態を参照している
    ● LauchedEffect, DisposableEffect の Key ではない状態を参照
    ● 時間が経過して使用される状態への参照
    ● コールバック関数
    82

    View Slide

  80. SideEffect
    83

    View Slide

  81. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    return analytics
    }
    SideEffect

    View Slide

  82. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    return analytics
    }
    SideEffect

    View Slide

  83. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    return analytics
    }
    SideEffect

    View Slide

  84. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    return analytics
    }
    SideEffect
    Userの状態が更新 → Recomposition
    1

    View Slide

  85. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    return analytics
    }
    SideEffect
    Userの状態が更新 → Recomposition
    Recomposition → SideEffect実行
    1
    2

    View Slide

  86. SideEffect のメリット
    89

    View Slide

  87. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    analytics.setUserProperty("user_type", user.type)
    return analytics
    }
    SideEffect
    これと同じ??

    View Slide

  88. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    analytics.setUserProperty("user_type", user.type)
    error("Error")
    return analytics
    }
    SideEffect
    エラー発生

    View Slide

  89. @Composable
    fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics = remember { Firebase.analytics }
    SideEffect {
    analytics.setUserProperty("user_type", user.type)
    }
    analytics.setUserProperty("user_type", user.type)
    error("Error")
    return analytics
    }
    SideEffect
    実行されない
    実行される

    View Slide

  90. SideEffect とログ
    93

    View Slide

  91. val listState = rememberLazyListState()
    LazyColumn(state = listState) {
    // ...
    }
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    SideEffect

    View Slide

  92. val listState = rememberLazyListState()
    LazyColumn(state = listState) {
    // ...
    }
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    SideEffect
    スクロールのたびに状態が変わる

    View Slide

  93. val listState = rememberLazyListState()
    LazyColumn(state = listState) {
    // ...
    }
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    SideEffect
    スクロールのたびに状態が変わる
    状態の参照 = Recomposition対象

    View Slide

  94. val listState = rememberLazyListState()
    LazyColumn(state = listState) {
    // ...
    }
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    SideEffect
    スクロールのたびに状態が変わる
    状態の参照 = Recomposition対象
    スクロールのたびに
    Recomposition

    View Slide

  95. val listState = rememberLazyListState()
    LazyColumn(state = listState) {
    // ...
    }
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    SideEffect {
    Timber.d("index = ${listState.firstVisibleItemIndex}")
    }
    SideEffect
    これが要因でRecompositionが発生しなくなる

    View Slide

  96. derivedStateOf
    99

    View Slide

  97. Box {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) { /* ... */ }
    val showButton = listState.firstVisibleItemIndex > 0
    if (showButton) {
    Button() {
    Text(text = "ScrollTop")
    }
    }
    }
    derivedStateOf

    View Slide

  98. Box {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) { /* ... */ }
    val showButton = listState.firstVisibleItemIndex > 0
    if (showButton) {
    Button() {
    Text(text = "ScrollTop")
    }
    }
    }
    derivedStateOf
    スクロールしている場合はボタンを表示

    View Slide

  99. Box {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) { /* ... */ }
    val showButton = listState.firstVisibleItemIndex > 0
    if (showButton) {
    Button() {
    Text(text = "ScrollTop")
    }
    }
    }
    derivedStateOf
    スクロールのたびに Recomposition
    スクロールのたびに状態が変わる

    View Slide

  100. Box {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) { /* ... */ }
    val showButton = listState.firstVisibleItemIndex > 0
    if (showButton) {
    Button() {
    Text(text = "ScrollTop")
    }
    }
    }
    derivedStateOf
    スクロールのたびに Recomposition
    スクロールのたびに状態が変わる
    ここが変わったときだけ
    Recompositionしてほしい

    View Slide

  101. Box {
    // ...
    val showButton = listState.firstVisibleItemIndex > 0
    val showButton by remember {
    derivedStateOf {
    listState.firstVisibleItemIndex > 0
    }
    }
    // ...
    }
    derivedStateOf
    derivedStateOf で結果が変わった時に
    Recompositionするようにする

    View Slide

  102. derivedStateOf を
    使わないほうが良い場合
    105

    View Slide

  103. @Composable
    fun Content(friends: List) {
    val friendsCount by remember {
    derivedStateOf {
    friends.size
    }
    }
    }
    derivedStateOf

    View Slide

  104. @Composable
    fun Content(friends: List) {
    val friendsCount by remember {
    derivedStateOf {
    friends.size
    }
    }
    }
    derivedStateOf
    サイズが変更されたら変数の値も更新

    View Slide

  105. @Composable
    fun Content(friends: List) {
    val friendsCount by remember {
    derivedStateOf {
    friends.size
    }
    }
    val friendsCount = friends.size
    }
    derivedStateOf

    View Slide

  106. produceState
    109

    View Slide

  107. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState

    View Slide

  108. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState
    初期値 0
    初期値 0

    View Slide

  109. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState
    1から10までを1秒ごとにループ

    View Slide

  110. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState
     value に値を設定
    1

    View Slide

  111. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState
     value に値を設定
    count に設定される
    1
    2

    View Slide

  112. val count by produceState(0) {
    (1..10).forEach {
    delay(1000)
    value = it
    }
    }
    Text(text = "Count = $count")
    produceState
     value に値を設定
    count に設定される
    表示
    1
    2
    3

    View Slide

  113. クリーンアップが必要な時
    116

    View Slide

  114. val count by produceState(0) {
    // ...
    awaitDispose {
    // ...
    }
    }
    produceState
     クリーンアップ処理

    View Slide

  115. snapshotFlow
    118

    View Slide

  116. snapshotFlow
    var count by remember { mutableIntStateOf(0) }
    LaunchedEffect(Unit) {
    snapshotFlow {
    count
    }.map {
    "Count = $it"
    }.collect {
    Timber.d(it)
    }
    }

    View Slide

  117. var count by remember { mutableIntStateOf(0) }
    LaunchedEffect(Unit) {
    snapshotFlow {
    count
    }.map {
    "Count = $it"
    }.collect {
    Timber.d(it)
    }
    }
    snapshotFlow
     更新されていくカウント

    View Slide

  118. var count by remember { mutableIntStateOf(0) }
    LaunchedEffect(Unit) {
    snapshotFlow {
    count
    }.map {
    "Count = $it"
    }.collect {
    Timber.d(it)
    }
    }
    snapshotFlow

    View Slide

  119. var count by remember { mutableIntStateOf(0) }
    LaunchedEffect(Unit) {
    snapshotFlow {
    count
    }.map {
    "Count = $it"
    }.collect {
    Timber.d(it)
    }
    }
    snapshotFlow
    State を Flow に変換

    View Slide

  120. var count by remember { mutableIntStateOf(0) }
    LaunchedEffect(Unit) {
    snapshotFlow {
    count
    }.map {
    "Count = $it"
    }.collect {
    Timber.d(it)
    }
    }
    snapshotFlow
    Flow の処理

    View Slide

  121. var firstName by remember { mutableStateOf("") }
    var lastName by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
    snapshotFlow {
    "$firstName $lastName"
    }.collect {
    Timber.d(it)
    }
    }
    snapshotFlow
    2つのStateを組み合わせた結果

    View Slide

  122. まとめ
    125

    View Slide

  123. まとめ
    ● LaunchedEffect
    ○ 状態が変わった時に何かしたい時
    ○ Coroutines
    ● rememberCoroutineScope
    ○ UIイベントからCoroutinesの処理をしたい時
    ● DisposableEffect
    ○ 何かクリーンアップが必要な時
    ● rememberUpdatedState
    ○ 変化する状態を参照している時
    126

    View Slide

  124. まとめ
    ● SideEffect
    ○ Compositionが成功した時に何かしたい時
    ○ ログ
    ● derivedStateOf
    ○ 状態から別の値を派生させたい時
    ● produceState
    ○ Compose外の状態をComposeのStateに変換したい時
    ● snapshotFlow
    ○ ComposeのStateの更新をCompose外でFlowで受け取りたい時
    127

    View Slide

  125. ありがとうございました
    128

    View Slide