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
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 */
}
}
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
โข