Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose の Side-effect を使いこなす / DroidKai...

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. 自己紹介 • Kenji Abe • Google Developers Expert for Android,

    Kotlin • DeNA Co., Ltd. • X: @STAR_ZERO • Bluesky: @star-zero.com 2
  2. Side effect (副作用) • 関数や操作などが結果を返すなどの主となる効果以外の効果 ◦ グローバル変数の変更 ◦ I/O操作 •

    ほかのSide effectを起こす関数の呼び出し • 実行順によって結果が変わる可能性がある • デバッグが困難になる • テストが難しくなる 4
  3. Side effectが必要な状況 • スナックバーを表示する • 1 回限りのイベントをトリガーする • 特定の状態で別の画面に移動する •

    などなど 10 https://developer.android.com/jetpack/compose/side-effects?hl=ja Side effectを安全に 実行する必要がある
  4. Side effect APIs • LaunchedEffect • rememberCoroutineScope • DisposableEffect •

    rememberUpdatedState • SideEffect • derivedStateOf • produceState • snapshotFlow 11 https://developer.android.com/jetpack/compose/side-effects?hl=ja
  5. LauchedEffect var showSnackbar by remember { mutableStateOf(false) } LaunchedEffect(showSnackbar) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    (showSnackbar) { snackbarHostState.showSnackbar( message = "OK" ) showSnackbar = false } } LauchedEffect false → true 5 Key が変更されたので 実行される 6
  20. rememberCoroutineScope val scope = rememberCoroutineScope() Button( onClick = { scope.launch

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

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

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

    snackbarHostState.showSnackbar("Hello, World") } } ) { Text(text = "Show Snackbar") } rememberCoroutineScope Composition から Leave するときにキャンセル
  24. 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) } }
  25. 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取得
  26. 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 と同じ感じ
  27. 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じゃない
  28. 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監視の処理
  29. 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 クリーンアップの処理
  30. var flag by remember { mutableStateOf(false) } DisposableEffect(flag) { //

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

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

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

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

    ... onDispose { // ... } } DisposableEffect 実行 5
  35. var count by remember { mutableIntStateOf(0) } Button(onClick = {

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

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

    count++ }) { Text(text = "Increment: $count") } Content(count = count) rememberUpdatedState 別のComposable関数へ渡す
  38. @Composable private fun Content(count: Int) { LaunchedEffect(Unit) { delay(5000) Timber.d("Count

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

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

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

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

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

    = $count") } } rememberUpdatedState 値が変わるたびに最初から数え直し
  44. @Composable private fun Content(count: Int) { val currentCount by rememberUpdatedState(count)

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

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

    LaunchedEffect(Unit) { delay(5000) Timber.d("Count = $currentCount") } } rememberUpdatedState 変換した変数を参照
  47. @Composable fun rememberAnalytics(user: User): FirebaseAnalytics { val analytics = remember

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

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

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

    { Firebase.analytics } SideEffect { analytics.setUserProperty("user_type", user.type) } return analytics } SideEffect Userの状態が更新 → Recomposition 1
  51. @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
  52. @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 これと同じ??
  53. @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 エラー発生
  54. @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 実行されない 実行される
  55. val listState = rememberLazyListState() LazyColumn(state = listState) { // ...

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

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

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

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

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

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

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

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

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

    val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } // ... } derivedStateOf derivedStateOf で結果が変わった時に Recompositionするようにする
  65. @Composable fun Content(friends: List<User>) { val friendsCount by remember {

    derivedStateOf { friends.size } } } derivedStateOf
  66. @Composable fun Content(friends: List<User>) { val friendsCount by remember {

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

    derivedStateOf { friends.size } } val friendsCount = friends.size } derivedStateOf
  68. val count by produceState(0) { (1..10).forEach { delay(1000) value =

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

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

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

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

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

    it } } Text(text = "Count = $count") produceState  value に値を設定 count に設定される 表示 1 2 3
  74. val count by produceState(0) { // ... awaitDispose { //

    ... } } produceState  クリーンアップ処理
  75. snapshotFlow var count by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) {

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

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

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

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

    { count }.map { "Count = $it" }.collect { Timber.d(it) } } snapshotFlow Flow の処理
  80. var firstName by remember { mutableStateOf("") } var lastName by

    remember { mutableStateOf("") } LaunchedEffect(Unit) { snapshotFlow { "$firstName $lastName" }.collect { Timber.d(it) } } snapshotFlow 2つのStateを組み合わせた結果
  81. まとめ • LaunchedEffect ◦ 状態が変わった時に何かしたい時 ◦ Coroutines • rememberCoroutineScope ◦

    UIイベントからCoroutinesの処理をしたい時 • DisposableEffect ◦ 何かクリーンアップが必要な時 • rememberUpdatedState ◦ 変化する状態を参照している時 126
  82. まとめ • SideEffect ◦ Compositionが成功した時に何かしたい時 ◦ ログ • derivedStateOf ◦

    状態から別の値を派生させたい時 • produceState ◦ Compose外の状態をComposeのStateに変換したい時 • snapshotFlow ◦ ComposeのStateの更新をCompose外でFlowで受け取りたい時 127