I have derivedStateOf that uses 3 different states: heightValue
, isOk
, and selectedOption
which was deconstructed in (selectedOption, onOptionSelected)
and possibly is the reason why. It will not respond to changes in selectedOption
unless I specify it in the remember(selectedOption)
. I'm trying to understand why and if my fix was the correct fix or should I have handled the deconstruction differently.
val radioOptions = listOf("A", "B", "C", "D")
var heightValue by remember { mutableIntStateOf() }
var isOk by remember { mutableStateOf(true) }
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) }
//val calculatedValue by remember() { /*This won't work. Next line is the fix*/
val calculatedValue by remember(selectedOption) {
derivedStateOf {
calcNewValueInch(
pickerValue = heightValue,
isOk = isOk,
selectedOption = selectedOption,
)
}
}
derivedStateOf
only works with State objects. selectedOption
is not a State, therefore any changes to it won't be detected.
Let's have a look at this in more detail. In your original, not working example with just remember()
, derivedStateOf
is executed on first composition. It also executes the lambda and keeps track of all State objects that were used. We expect that to be the following:
heightValue
isOk
selectedOption
Then the result of derivedStateOf
is remember
ed so it won't be executed again on recomposition. That's ok: The now created derived state automatically updates when any of the States in the list above changes.
Except it only works for 1. and 2., it doesn't work for 3.
Since derivedStateOf
only keeps track of actual State objects that were used in its lambda, it is quite obvious that it won't update when 3. changes: selectedOption
is just a simple String, not a State. The more interesting question is why it does work for 1. and 2. though, since they are also no States, just an Int and a Boolean. But that's not entirely true. They are actually delegates that you retrieved by using the by
keyword. What that means is that you can use them as simple Int and Boolean values, but whenever their value is read, the getValue
function of the delegated object is actually executed. When their value is set, setValue
will be called instead. Kotlin hides all that from you so your code looks cleaner, but that is what actually happens when a delegate is accessed. In conclusion, 1. and 2. are delegates to a State, and therefore derivedStateOf
can see those States and monitor their changes.
selectedOption
, however was created by deconstructing the MutableState. That leaves you with a simple String containing the value at the time of deconstruction. The tricky thing is that for most cases this will be sufficient, so it doesn't feel that much different than the delegated States:
Text(selectedOption)
This will work as expected and updates everytime the underlying State of selectedOption
was changed. That's because the Compose runtime keeps track of all State reads, and deconstructing a State counts as such. When the State is changed, it recomposes the function, so the deconstruction is repeated and selectedOption
is updated. But the derivedStateOf
lies behind a remember
and isn't affected by such recompositions. That's why it keeps track of all State objects itself. But it never saw the deconstruction of the State behind selectedOption
, so it only sees the result, which is a simple String, not a State.
We can now revise the above list of States that derivedStateOf
keeps track of:
heightValue
delegateisOk
delegateselectedOption
, since that is just a simple StringThe only States that are accessed in the lambda are 1. and 2., so whatever you do with 3., it won't affect the derivedStateOf
.
When you use remember(selectedOption)
it seems to work, but what actually happens is that the entire derived State is thrown away each time selectedOption
changes and a new one is created. The lambda is executed again and the above list of State objects that should be observed for changes is created, but it doesn't matter that 3. is not actually on the list, because you just create a new derived State object when that changes, you don't let the current derived State object update.
The proper solution to the problem is to keep the State behind selectedOption
around. Either by also delegating it like you did with 1. and 2., or if for whatever reason you do not want to do that, by something like this:
val selectedOptionState = remember { mutableStateOf(radioOptions[0]) }
val (selectedOption, onOptionSelected) = selectedOptionState
val calculatedValue by remember {
derivedStateOf {
calcNewValueInch(
pickerValue = heightValue,
isOk = isOk,
selectedOption = selectedOptionState.value, // this accesses the State now
)
}
}
In that case I would advise you to remove the deconstruction entirely and only access selectedOptionState.value
so the reader of your code isn't left to wonder why you sometimes use this and sometimes you use that to access the same State.