Reputation: 3931
If I'm understanding the documentation correctly a LaunchedEffect should not run again if the rememberUpdatedState has not changed.
If I run something like this code below then it's not working as expected and the value is getting updated again on rotation.
Without the LaunchedEffect the rememberSaveable is remembered on config change and the text in the input is correct (if I type something it's still there). This leads me to believe that the rememberUpdatedState should also not have changed but yet it gets triggered. Why?
What am I doing wrong or is this a bug? Alternatively is there a better way to do this?
Thanks :)
@Composable
fun ThingView(
thingViewModel: ThingViewModel,
id: String?
) {
var thingName by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope()
LaunchedEffect(rememberUpdatedState(newValue = thingName)) {
scope.launch {
id?.let {
val thing = thingViewModel.getThing(id)
thingName = thing.name
}
}
}
OutlinedTextField(
value = thingName,
onValueChange = { thingName = it },
label = { Text("Name") }
)
}
Edit:
To clarify, the goal is to allow the user enter text in a textField and not have that text cleared on rotation. That would be super annoying for a user and probably not what they expect.
Upvotes: 6
Views: 4001
Reputation: 2505
I think what you are seeing is that the LaunchedEffect
state gets cleared on rotation when the configuration changes. Consequently I don't think there is any value that you can provide as a key to keep it from running.
There are two options that come to mind. In the style of your original code, you might consider only declaring the LaunchedEffect
when you determine the state has changed. For example, you might maintain the last seen id
in a saveable, say lastId
, and test for changes on entry. It requires you have a sentinel value for lastId
initially of course.
@Composable
fun ThingView(
thingViewModel: ThingViewModel,
id: String?
) {
var thingName by rememberSaveable { mutableStateOf("") }
var lastId: String? by rememberSaveable { mutableStateOf(null) }
val scope = rememberCoroutineScope()
if (lastId != id) {
lastId = id
LaunchedEffect(id) {
scope.launch {
id?.let {
val thing = thingViewModel.getThing(id)
thingName = thing.name
}
}
}
}
OutlinedTextField(
value = thingName,
onValueChange = { thingName = it },
label = { Text("Name") }
)
}
Another option that might be more traditional is to use the ViewModel
to persist the state across configuration changes. You might update the ViewModel on each onValueChange
and have the ViewModel
provide the current value back to the OutlinedTextField
. In this style you might not need the effect or the coroutine.
Just a last word regarding my understanding about rememberUpdatedState
. I believe it is typically intended to allow a long-lived coroutine or callback to reference the current value of a variable in the composable scope without requiring that the coroutine or callback be recreated on each state change. The closure seems to be scoped to the effect and it capture the values at the time of creation. I am still trying to understand it better.
Upvotes: 6