julioromano
01/24/2022, 3:01 PMderivedStateOf
track reads of State
objects at run time or compile time (or perhaps I should say dynamically or statically)?
🧵val aMutableListOfStrings = mutableListOf<String>()
val aSnapshotStateListOfStrings = mutableStateListOf<String>()
val myStuff: List<String> by derivedStateOf {
aMutableListOfStrings.filter {
aSnapshotStateListOfStrings.contains(it)
}
}
The edge case happens when, at the very first run of the derivedStateOf
lambda, aMutableListOfStrings
is empty.
In this case no actual reads of aSnapshotStateListOfStrings
take place at run time.
Now when the contents of aSnapshotStateListOfStrings
changes, the derivedStateOf
lambda is not run again: even if by then aMutableListOfStrings
is not empty anymore (and therefore this time it would actually lead to reads of aSnapshotStateListOfStrings
).
I have the impression that this happens because the derivedStateOf
lambda tracks reads to state objects at runtime during its first run.
Since no reads were tracked in this edge case conditions, the logic assumes this derivedState can never change.
I initially thought this edge case couldn’t happen because somehow the snapshot state system would be able to track the read to aSnapshotStateListOfStrings
because it actually appeared in a statement in the code (as if reads were tracked statically, e.g. by code analysis) but then I though this might not be the case and that tracking might happen dynamically at runtime.
I’m not sure whether to call this a bug (I’m more inclined to think this is working as intended) but I just wanted to make sure this is actually the behavior I’m witnessing in my code.Albert Chang
01/24/2022, 3:17 PMjulioromano
01/24/2022, 3:23 PMfilter
on an empty list never runs the filter’s lambda. Therefore the contains()
on the SnapshotStateList
list is never invoked.nitrog42
01/24/2022, 3:35 PMderivedStateOf
; so if you expect the state to update when the list change, you need your simple list to be a statejulioromano
01/24/2022, 3:37 PMaSnapshotStateListOfStrings
changes (that’s why it is the only SnapshotStateList
).Albert Chang
01/24/2022, 3:46 PMfilter
know the list is empty?julioromano
01/24/2022, 3:50 PMfilter
is implemented with a for loop: when run, if the list you are filtering is empty, its lambda will never be called so aSnapshotStateListOfStrings
will never be read.Albert Chang
01/24/2022, 3:53 PMif the list you are filtering is emptyHow does the for loop know the list is empty?
size
property of the list is read, and thus recorded.nitrog42
01/24/2022, 3:54 PMval list = mutableListOf<String>()
Albert Chang
01/24/2022, 3:56 PMaMutableListOfStrings
isn’t a state list. So this is expected.Zach Klippenstein (he/him) [MOD]
01/24/2022, 5:28 PMjulioromano
01/24/2022, 8:16 PMAlex Vanyo
01/24/2022, 10:03 PMprivate var switch
is a plain var
, so Compose has no way of tracking when it changes.
The solution to get the behavior you’d expect is private var switch: Boolean by mutableStateOf(false)
, so that derivedStateOf
can re-run if switch
changes.julioromano
01/25/2022, 8:55 AMstate
even though the read is conditional on the value of switch
(which is a plain var, no mutableStateOf ) so in principle the derived lambda should be rerun whenever state
changes, regardless of the value of switch
. But this doesn’t happen.
My conjecture is that this most likely happens because the runtime decides whether to track state
based on whether there had been any actual reads from it during the lambda’s first run.
So if, during this first lambda run, switch
is false then state
isn’t going to be tracked by this derived state.
IMHO this is the result of a limitation of the system (i.e. the fact that tracking of reads of state vars happens dynamically at runtime). If the tracking were to be based e.g. on statical source code analysis, then the read to state
would always be tracked (because the read statement is actually in the code: state.uppercase()
).
N.B. I’m not asking nor suggesting to do statical state tracking, I’m just trying to understand why this is happening and, if my conjecture is actually true, perhaps we can improve the docstring of derivedState
highlighting this peculiarity.nitrog42
01/25/2022, 9:15 AM@Composable
@ReadOnlyComposable
private fun resources(): Resources {
LocalConfiguration.current
return LocalContext.current.resources
}
where LocalConfiguration.current
is used to track changes and retrigger the get of resources instanceprivate val derived: String by derivedStateOf {
state
if (switch) {
state.uppercase()
} else {
"totallyBogusStuff"
}
}
just to track change, which is weird and you have to know "why", but it would works
not sure if there will be any other solution for this (inside the framework/compiler I mean)julioromano
01/25/2022, 9:17 AMAlbert Chang
01/25/2022, 9:32 AMjulioromano
01/25/2022, 9:35 AMAlbert Chang
01/25/2022, 9:48 AMAlex Vanyo
01/25/2022, 5:07 PMprivate val derived: String by derivedStateOf {
if (someFunction()) {
state.uppercase()
} else {
"empty"
}
}
When the block runs, derivedStateOf
will call someFunction
, and if the result is true then, it will read the state
.
If someFunction
returns false
, then state
won’t be read, so from the perspective of derivedStateOf
, there’s no way that any changes to state
can impact the outcome.
So how can the result of derivedStateOf
ever change? By changing any snapshot state that someFunction
read to make its decision.
The power is that someFunction
can refer to state that derivedStateOf
has absolutely no knowledge about. someFunction
could be delegated to an interface, with various logic, to another class, 5, 10 levels deep. If someFunction
depends on 5 pieces of snapshot state, then the only way for someFunction
to change is if any of those 5 pieces change. If that happens, then derivedStateOf
will run again.
Maybe the new result is still false
, but it depends on 10 pieces of different state this time. If any of those 10 pieces change, then we calculate again.
The other thing is that this isn’t unique to derivedStateOf
, this is the behavior throughout the Compose system.
The limitation here is that you have to work with the Compose snapshot state system throughout. If you refer to some var flag: Boolean
, then from the perspective of Compose, it’s like doing
if (false) state else "empty"
. From the perspective of Compose, there’s no way for that false
to ever become true. (this gets tricky for state managed outside of Compose, and the resources
is a great example: the resources can change arbitrarily, so we need to depend on the LocalConfiguration
as a Compose hook to trigger rerunning)
The power of this system is that you don’t have to manually wire up all of the possible dependencies in a massive Flow<T>
network (or choose your other observable type of choice). Each piece of internal state can independently and transparently create new state based on other state.julioromano
01/25/2022, 7:39 PM