As I am doing this codelab (Step 4) from Android Developer website, I noticed it is said that the callback function can be changed even after it is passed to the Composable, and the code needs to protect it against changes. As below:
Some side-effect APIs like LaunchedEffect take a variable number of keys as a parameter that are used to restart the effect whenever one of those keys changes. Have you spotted the error? We don't want to restart the effect if onTimeout changes!
To trigger the side-effect only once during the lifecycle of this composable, use a constant as a key, for example LaunchedEffect(true) { ... }. However, we're not protecting against changes to onTimeout now!
If onTimeout changes while the side-effect is in progress, there's no guarantee that the last onTimeout is called when the effect finishes. To guarantee this by capturing and updating to the new value, use the rememberUpdatedState API:
The code:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
@Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// 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 or onTimeout changes,
// the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTime)
currentOnTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
I am confused about how a callback function (onTimeout in this case) can be changed as the code doesn't make any modification on it. What I am understanding is, the onTimeout callback is saved as a State in the memory, is forgot/deleted when the Composable exits the Composition, and is re-initialized during Recomposition, which implies change. Therefore we have to use rememberUpdatedState to ensure the last used onTimeout (rather than an empty lambda because Composable doesn't care about execution order) is passed to the LaunchedEffect scope during Recomposition
However all above are just my assumptions since I am still new with this topic. I have read some documentation but still not fully understood. Please correct me if I am wrong or help me understand it in a more approachable way.
Thanks in advance
CodePudding user response:
In the example in Codelab provided timeout doesn't change but it's fictionalized as it might change many default Composable use
rememberUpdatedState
which is
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
As in your question and comment below it could also be used as
var currentOnTimeout by remember(mutableStateOf(onTimeout))
currentTimeout = onTimeout
which looks less nicer than rememberUpdatedState but both works. It's a preference or option you can choose from for same end.
Slider
and many other Composables use rememberUpdatedState
too
@Composable
fun Slider(
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
/*@IntRange(from = 0)*/
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SliderColors = SliderDefaults.colors()
) {
require(steps >= 0) { "steps should be >= 0" }
val onValueChangeState = rememberUpdatedState(onValueChange)
}
even if you onValueChange
function you provide won't change most of the time, probably in few instances it might need to change, uses rememberUpdatedState
to make sure latest value of callback is passed to Slider
.
Slider(
value = value,
onValueChange = {
value it
}
)
In the example below Calculation2 Composable enters composition when showCalculation is true and passes a callback named operation. While calculation is going on if you change selection Calculation2 function recomposed with new operation callback. If you remember old value instead of updating it after delay ends LaunchedEffect calls outdated operation that's why you use rememberUpdatedState. So, while functions recompose callback that they receive can change and you might need to use latest one.
/**
* In this example we set a lambda to be invoked after a calculation that takes time to complete
* while calculation running if our lambda gets updated `rememberUpdatedState` makes sure
* that latest lambda is invoked
*/
@Composable
private fun RememberUpdatedStateSample2() {
val context = LocalContext.current
var showCalculation by remember { mutableStateOf(true) }
val radioOptions = listOf("Option