Android Compose Side-effect


A side-effect is a change to the state of the app that happens outside the scope of a composable function.


- rememberUpdatedState

You might want to capture a value in your effect.

If it value changes, you do not want the effect to restart.

The rememberUpdateState create a reference to this value which can be captured and updated.

@Composable
fun ProfileScreen(username: String) {
var messageVisible by remember { mutableStateOf(false) }
var message by remember { mutableStateOf("") }

// capture username's latest value
val currentUsername by rememberUpdatedState(username)

LaunchedEffect(messageVisible) {
if (messageVisible) {
delay1(duration = 3.seconds)
messageVisible = false
}
}

Column {
Text(text = "현재 사용자: $username")
Button(onClick = {
message = "${currentUsername}, 안녕하세요!"
messageVisible = true
}) {
Text("환영 메시지 표시")
}

if (messageVisible) {
Text(message)
}
}
}

@Composable
fun ExampleUsage() {
var currentUsername by remember { mutableStateOf("손흥민") }

Column {
ProfileScreen(username = currentUsername)
Button(onClick = {
currentUsername = "김민재"
}) {
Text("사용자 이름 변경")
}
}
}


-DisposableEffect

For side effects that need to be cleaned up after the keys change or if the composable leaves the Composition

If the DisposableEffect keys change, the composable needs to do dispose its current effect, and reset by calling the effect again.


@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 */
}

As an example, you might want to send analytics events baed on Lifecycle events/

To listen for those events in Compose, use a DisposableEffect to register and unregister the observer when needed.


-SideEffect

To share Compose state with objects not managed by compose, use the sideEffect composable.

Using a SideEffect guarantees that the effect executes after every successful recomposition.


@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}

// 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

produceState launches a coroutine scoped to the Composition that can push values into a returned State.

Use it to convert non-Comose state into Compose state.

@Composable
fun loadNetworkImage(
url: String,
imageRepository: 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)
}
}
}

This example shows how to use produceState to load an image from the network.

The loadNetworkImage composable function returns a State that can be used in other composables.


-derivedStateOf

In Compose, recomposition occurs each time an observed state object or composable input changes.

A state object or input may be changing more often than UI actually needs to update, leading to unnecessary recomposition.

You should use the derivedStateOf function when your inputs to a composable are changing more often than you need to recompose.

derivedStateOf creates a new Compose state object you can observe that only updates as much as you need.


@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
Box {
val listState = rememberLazyListState()

LazyColumn(state = listState) {
// ...
}

// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}

AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
}
}

On example code, firstVisibleItemIndex changes any time the first visible item changes.

However, recomposition only needs to occur if the value is greater than 0.

In this case, derivedStateOf reduce unnecessary recomposition.