Reputation: 45
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
Upvotes: 1
Views: 4402
Reputation: 67169
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🍒", "Option🍏", "Option🎃")
val (selectedOption: String, onOptionsSelected: (String) -> Unit) = remember {
mutableStateOf(radioOptions[0])
}
Column {
radioOptions.forEach { text ->
Column(
modifier = Modifier.selectableGroup()
) {
Row(
Modifier
.selectable(
selected = (text == selectedOption),
onClick = {
if (!showCalculation) {
showCalculation = true
}
onOptionsSelected(text)
},
role = Role.RadioButton
)
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
RadioButton(selected = (text == selectedOption), onClick = null)
Spacer(modifier = Modifier.width(16.dp))
Text(text = text)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
if (showCalculation) {
println("📝 Invoking calculation2 with option: $selectedOption")
Calculation2 {
showCalculation = false
Toast.makeText(
context,
"Calculation2 $it result: $selectedOption",
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
/**
* LaunchedEffect restarts when one of the key parameters changes.
* However, in some situations you might want to capture a value in your effect that,
* if it changes, you do not want the effect to restart.
* In order to do this, it is required to use rememberUpdatedState to create a reference
* to this value which can be captured and updated. This approach is helpful for effects that
* contain long-lived operations that may be expensive or prohibitive to recreate and restart.
*/
@Composable
private fun Calculation2(operation: (String) -> Unit) {
println("🤔 Calculation2(): operation: $operation")
// This returns the updated operation if we recompose with new operation
val currentOperation by rememberUpdatedState(newValue = operation)
// This one returns the initial operation this Composable enters composition
val rememberedOperation = remember { operation }
// 🔥 This LaunchedEffect block only gets called once, not called on each recomposition
LaunchedEffect(key1 = true, block = {
delay(4000)
currentOperation("rememberUpdatedState")
rememberedOperation("remember")
})
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(color = getRandomColor())
}
}
You can also consider more practical situation while you load data from remote server user might interact with your app like the example above and you might need to navigate based on user's latest interaction after loading is done.
Upvotes: 1