Anzeige
Anzeige

Más contenido relacionado

Similar a Side Effect in Compose (Android Jetpack)(20)

Último(20)

Anzeige

Side Effect in Compose (Android Jetpack)

  1. Side-effects in Compose 이상수(vviprogrammer@gmail.com)
  2. Side-effect 란? • ISO/IEC 14882 • Accessing an object designated by a volatile lvalue, modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. • 쉽게 말해서 • 실행 중에 어떤 객체에 접근해서 변화가 일어나는 행위 (라이브러리 등 I/O, 객체 변경 등) • X = 3 + 4
  3. Side-effect • Composable Scope 외부에서 일어나 는 앱의 상태 변화 • 이상적인 Composable 은 side-effect 에 대해서 free 해야 함 • 하지만, 필요한 경우, 적절한 사용이 필요 • Ex) 일회성 이벤트, 특정 조건 시 다른 화면으로 Navigate 등 • 보통 Controlled Env. 에서 실행 in Compose
  4. Effect API 컴포즈에서 Side-effect 가 필요할 때 • 기본적인 Compose 의 컨셉은 side-effect 에 대해 Free 함을 전제 • Effect API 를 사용 : 예측 가능한 범위에서 side-effect 를 실행 • Effect 는 Composable 함수이며 UI 를 emit 하지 않음 https://developer.android.com/jetpack/compose/mental-model
  5. 🚀LaunchedEffect • suspend 함수를 Composable 내 에서 안전하게 호출하기 위해 사용 • 컴포지션 진입 시 전달된 블록을 코루 틴으로 실행 • Ex) Scaffold 안에서 Snackbar suspend 함수를 Composable Scope 에서 실행 시키기 @Composable fun MyScreen( state: UiState<List<Movie>>, scaffoldState: ScaffoldState = rememberScaffoldState() ) { if (state.hasError) { LaunchedEffect(scaffoldState.snackbarHostState) { scaffoldState.snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold(scaffoldState = scaffoldState) { /* ... */ } }
  6. rememberCoroutineScope • vs `LaunchedEffect` • composable 영역이 아닌 곳에서 코 루틴을 수행 • Composition 을 벗어나면 자동으로 취소되는 스코프 • Composable 함수이며, 호출된 Composition 지점에 bind 되는 CoroutineScope 를 반환 composable 외부에서의 코루틴 @Composable fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) { // Composition-aware scope val scope = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { Column { /* ... */ Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { scaffoldState.snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
  7. ⭐ rememberUpdatedState • 상태 값이 바뀌는 경우 재실행되면 안 되는 하나의 effect 안에서 값을 참조 하는 방법 • LaunchedEffect 의 key 로 상수값 을 설정하여 호출된 곳의 수명 주기를 따르도록 할 수 있음 • rememberUpdatedState 를 사용 하여 상위 컴포저블의 최신 상태 값을 유지할 수 있음 최신의 상태를 업데이트 받는 State @Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
  8. DisposableEffect • key 의 변경 or Composable 이 Composition 을 벗어나는 경우 clean- up 이 필요한 side effect. • key 등이 변경되는 경우 dispose 로 정리 해주며, 다시 실행 • Ex) lifecycleOwner 가 변경되는 경우 새 로운 lifecycleOwner 로 실행, event observer 등록/해제 등. • 반드시, 내부 마지막 절로 `onDispose` 를 작성 해야함. 리소스 Clean-up 이 필요한 Effect @Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
  9. SideEffect • Compose 상태를 non-Compose 상태에 게시 • Composition이 완료되고 호출됨 • Ex) FirebaseAnalytics 를 현재 사용 자의 userType 으로 업데이트 하여 메 타 데이터로 첨부 • 참고) return 값을 갖는 Composable 은 일반 kotlin 함수와 동일한 naming 규칙을 사용 Publish Compose State 👉 non-Compose State @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 }
  10. produceState • Composition 으로 값을 변환된 State 로 push 하는 Coroutine 을 Launch • non-Compose State 를 Compose State 로 변환 하고자 할 때 (Flow, LiveFata 또는 RxJava 등의 구독 기반 상태들) • 생산자(producer) 는 Composition 에 진입 하면 시작되고, Composition 에서 벗어나는 경우 취소된다. • 반환된 State는 합쳐지므로(Conflates), 동일 값이 설정되는 경우에는 re-composition 이 트리거되지 않음 Convert non-Compose State 👉 Compose State // produceState 를 사용하여 네트워크에서 이미지를 로드하는 방법 @Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
  11. produceState • Flow 를 State 로 변경 시 produceState 를 사용하고 있음 • LiveData 의 경우 observer 를 붙이고 떼는 작업을 위해 DisposableEffect 를 사용하고 있음 Convert non-Compose State 👉 Compose State @Composable fun <T : R, R> Flow<T>.collectAsState( initial: R, context: CoroutineContext = EmptyCoroutineContext ): State<R> = produceState(initial, this, context) { if (context == EmptyCoroutineContext) { collect { value = it } } else withContext(context) { collect { value = it } } } @Composable fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> { val lifecycleOwner = LocalLifecycleOwner.current val state = remember { mutableStateOf(initial) } DisposableEffect(this, lifecycleOwner) { val observer = Observer<T> { state.value = it } observe(lifecycleOwner, observer) onDispose { removeObserver(observer) } } return state }
  12. ⭐DerivedStateOf • 특정 상태가 다른 State Object 로 부 터 계산 되었거나, 파생된 경우 사용 • 계산에 사용된 상태 중 하나가 변경 될 때마다 연산이 발생 함을 보장 • 핵심: UI 를 업데이트 하려는 것보다 State 또는 key 가 더 많이 변경 될 때 • 마지막에 더 자세히..! 하나 또는 여러 상태 객체를 다른 상태로 변환 // Calculate high priority tasks only when the todoTasks or highPriorityKeywords // change, not on every recomposition val highPriorityTasks by remember(highPriorityKeywords) { derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } } } @Composable fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) { val todoTasks = remember { mutableStateListOf<String>() } val highPriorityTasks by remember(highPriorityKeywords, derivedStateOf) { todoTasks.filter { it.containsWord(highPriorityKeywords) } } Box(Modifier.fillMaxSize()) { LazyColumn { items(highPriorityTasks) { /* ... */ } items(todoTasks) { /* ... */ } } /* Rest of the UI where users can add elements to the list */ } }
  13. snapshotFlow • State<T> 객체를 cold Flow 로 변경 • collected 되면 블록을 실행, 읽어진 State 객체 상태를 emit • value 가 이전과 동일하면 emit 하지 않음 • Ex) 목록의 첫 번째 항목을 지나서 스크 롤 되는 경우 (변경 시 마다 한번만 처리) • 또한 Flow 연산자의 이점을 활용할 수 있게됨. Compose State 👉 Flow val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState. fi rstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() . fi lter { it == true } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
  14. Restarting Effect • Effect API 함수 몇몇은 가변 인자로 key 를 받을 수 있음 • key 가 변경되면 실행중인 effect 를 cancle 및 새로운 effect 를 실행 • 일반적으로 effect 블록에 사용되는 가변 및 불변 변수는 effect api에 키로 추가해야 함 • 또는 변수 변경으로 인해 effect 가 재시작되지 않아야 하는 경우에는 rememberUpdatedState 로 래핑하여 사용 해야 함. Effect 를 다시 시작 시키기 EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block } @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) } } }
  15. 🔑Constants as keys • Effect 의 key 로 상수를 사용하면 호출 지점의 수명 주기를 따르도록 할 수 있다. • ⚠ 하지만 LaunchedEffect(true) 같은 코드는 while(true) 만큼이나 사용 시 주의해야 함. (꼭 필요한 적합한 경우만 사용 하자) Effect 의 key 로 상수 사용 LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() }
  16. DerivedStateOf 좀 더 자세히 알아보기
  17. https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  18. https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  19. https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  20. DerivedStateOf 를 사용하기 적합한 사례 • 입력과 출력 결과 사이에 변동이 큰 경우 • 몇 가지 예시 사례 • 스크롤 값이 임계값을 넘었는지 관찰 (scrollPosition > 0) • 리스트의 아이템이 임계값 보다 큰지 (item > 0) • format 의 유효성 검사 (username.isValid())
  21. 🙋FAQ • Composable 내부에서는 보통 re- composition 시 유지해야 하므로 remember 로 래핑 • 또는 ViewModel 등 활용 DerivedStateOf 를 remember 해야 하나요? @Composable fun MyComp() { // We need to use remember here to survive recomposition val state = remember { derivedStateOf { … } } } class MyViewModel: ViewModel() { // We don't need remember here (nor could we use it) because the ViewModel is // outside of Composition. val state = derivedStateOf { … } }
  22. 🙋FAQ • remember(key) 와 derivedStateOf 의 차이는 re-composition 의 횟수와 관련있음 • Case 1 • 불필요한 re-composition을 버퍼링 • Case 2 • 입력=출력=re-composition remember(key) 와 derivedStateOf 의 차이점? val result = remember(state1, state2) { calculation(state1, state2) } val result = remember { derivedStateOf { calculation(state1, state2) } } val isEnabled = lazyListState.firstVisibileItemIndex > 0 val isEnabled = remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } } Case 1 Case 2 val output = remember(input) { expensiveCalculation(input) }
  23. 🙋FAQ • DerivedStateOf 는 Compose State 객체를 읽을 때만 업데이트 됨 • DerivedStateOf 가 생성 될 때 내부 에서 읽은 다른 모든 변수는 초기 값을 캡처하게 됨 • 연산 시 이런 변수를 사용해야 한다면 remember의 key 로 제공 해야 함 remember(key)와 DerivedStateOf 를 동시에 사용해야할 때? @Composable fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) { // There is a bug here val isEnabled by remember { derivedStateOf { lazyListState. fi rstVisibleItemIndex > threshold } } Button(onClick = { }, enabled = isEnabled) { Text("Scroll to top") } } https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  24. 🙋FAQ • remember(key) 를 사용하여 해결 remember(key)와 DerivedStateOf 를 동시에 사용해야할 때? @Composable fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) { val isEnabled by remember(threshold) { derivedStateOf { lazyListState. fi rstVisibleItemIndex > threshold } } Button(onClick = { }, enabled = isEnabled) { Text("Scroll to top") } } https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  25. 🙋FAQ • 결과를 연산하기 위해 사용되는 다중 상태가 있는 경우 • 오버헤드 발생 가능 • 결론 : UI update < state, key update 시에 사용 하는 것이 좋음 여러 상태를 합치기 위해 DerivedStateOf 는 적합한가? var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } // This derivedStateOf is redundant val fullName = remember { derivedStateOf { "$firstName $lastName" } } var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullName = "$firstName $lastName"
  26. References • https://developer.android.com/jetpack/compose/side-effects • Android UI Development with Jetpack Compose, Thomas Kunneth • https://developer.android.com/jetpack/compose/mental-model • https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use- derivedstateof-63ce7954c11b • https://hyeonk-lab.tistory.com/43 • https://romannurik.github.io/SlidesCodeHighlighter/ • https://tourspace.tistory.com/412 •
  27. 감사합니다
  28. Q&A
Anzeige