Hello, I'm trying to derive a state, based on its ...
# compose
l
Hello, I'm trying to derive a state, based on its previous value, and I'm unsure how to do it. I have a
State<Boolean>
, and I want to get a
MutableIntState
(or a
State<Int>
) that counts the times the boolean became
true
. I want to avoid recomposing/re-rendering when the boolean becomes
false
, so I think
derivedStateOf
is part of the solution, but I haven't found how to make it aware of its last value… I am thinking about making a class that wraps a
var
with that counter, but that seems ugly to me. Any other ideas?
v
what are you trying to do? 😅
Could you just count the invocations at the source? Like in a setter function, instead of reacting to changes to the state?
l
Did that for now (where
isTrue
is backed by a
State<Boolean>
) :
Copy code
var count by remember {
    MutableStateFlow(0)::value
}
val becameTrueCounter = remember {
    derivedStateOf {
        if (isTrue) ++count else count
    }
}
y
I think I'd combine a snapshotFlow with a runningFold, but not sure it's better.
z
I’ve talked with @Chuck Jazdzewski [G] a little bit about a derivedStateOf variant that takes a function that can see the previous value. This seems like a good use case for that although there are definitely workarounds. Feel free to file a feature request. And would you mind if i stole this use case for a talk I’m giving about derived state in a few weeks? 😜
👀 1
I wouldn’t update non-snapshot state in a derived state calculation block. If the snapshot changing the state is discarded, it will probably break
v
I've sometimes also taken the approach to create a "linked list" of length two when dealing with similar cases, the class keeps a reference to the old state that can be used down in the logic flow to trigger actions
It's not optimal either 😅 but it is a common use case to need to react when something changes (event), not just when some thing is (state)
z
i would probably do flow+scan, it’s probably the least code since the operators are pre-built, although it isn’t as “pure”.
Zooming out, though, it is technically not really sensical to count a number of state changes based on snapshot state (classic “state vs events” conflation). Because what you’re really counting is something like “the number of times a snapshot was applied in which this state was true after a snapshot was applied in which it’s false”, which doesn’t mean the value will have actually been set that many times (it may have toggled within a snapshot, or child snapshots). If you own the entire chain of state dependencies you could create a custom state object that tracks write counts as well as values, but it sounds like you might not own your state dependencies?
c
@Zach Klippenstein (he/him) [MOD] hits on the main problem with using the snapshot system to count things, it can't. The snapshot system takes point-in-time snapshots of data and no record is recorded when, if, or how many times, each value has changed. Once a snapshot starts, duplicate writes to the same value are ignored and changing a value back to its original value is also ignored and is reported as a change. It is up to the listener to determine if a change actually occurred; all the snapshot system tells you is that a write occurred to a instance and allows retrieving a current value relative to a snapshot. The fundamental issue is that the snapshot system tracks state not events. Its job is to maintain point-in-time consistency between multiple mutable values. What happens in between these point-in-time snapshots is not relevant and is not tracked. If you need to count events like the changing of a Boolean you need a different system such as
Flow<T>
which was designed to track events not state. You then can use
collectAsState()
to take a point-in-time snapshot of the flow when needed in composition which samples the flow at a particular point in time.
l
For my use case, I don't actually care about the count, I only want to trigger a re-render/recomposition when the initial state changes from false to true.
c
That is still an event so snapshots really cannot help you much unless the value is monotonic. That is, if it only changes from
false
to
true
and never back to
false
then a normal
mutableStateOf<Boolean>()
works fine. If it isn't monotonic then the snapshot system will only be able to tell you if it was written to and what the current value is. If it appears in a apply changes set then it depends on the mutation policy why it is there. For an structural policy it will only appear there if was ever written to with a different value than current if the initial value is
false
then you can assume if it is ever sent then it was sent because it was changed, at some point, from
false
to
true
but that only works for Booleans as they only have two states. In general, the snapshot system cannot be used for anything that requires some A to occur whenever some B occurs. It can only ensure A will happen if B may have occurred and then only if a snapshot was applied while B was in the desired state. BTW, never mutate state in the callback from a
dervivedStateOf
as the example above has. The callback function should be a pure function of the state it reads. Anything else is undefined behavior.
z
If we had a
derivedStateOf
that took the previous state, then you could avoid invalidating readers when it changes back to false. But if you don’t need the count, then you have some more options. E.g. Something like this could work. It’s two state reads per value call while the boolean is initially false, but then only a single state read. If your dependencies toggle very frequently (which is the main reason to consider using
derivedStateOf
at all anyway) then the cost of two state reads initially might be worth avoiding future invalidations, and it will quickly change to a single state read. When it changes it will be reported as a state write and invalidate other readers, but this will only happen once. You can avoid that by wrapping “state read 1” in a
Snapshot.withoutReadObservation
(which adds some cost) or if you implement a custom
StateObject
. This approach could be used for any type of value that you want to stop invalidating when it becomes a certain thing.
Copy code
class DerivedStateLatch(
  private val holdAt: Boolean = true,
  private val calculate: () -> Boolean
): State<Boolean> {
  private var stateHolder: State<Any> by mutableStateOf(derivedStateOf(calculate))

  override val value: Boolean
    get() {
      // State read 1: Figure out if we've reached the holdAt value yet or not.
      val stateValue = this.stateHolder
      if (stateValue is Boolean) {
        // The holdAt value has already been reached, return it.
        return stateValue
      }

      // State read 2: The holdAt value has not been reached yet, but might be on
      // this read.
      val currentValue = (stateValue as State<Boolean>).value
      if (currentValue == holdAt) {
        // The holdAt value was reached for the first time. Replace the derivedStateOf
        // inside stateHolder with the fixed value so on the next get() call we hit
        // the fast path above.
        this.stateHolder = currentValue
      }
      return currentValue
    }
}
However, it’s possible for the value to become true, then false again, without this code (or any code) ever seeing the true value. If you need that guarantee, you’d need to either use a custom state object or do something with flows.
l
If it becomes false again quickly and my code misses it, it's okay as well.
Given I only care about when the boolean becomes true, and stays so for at least a frame (i.e. missing when it becomes true before quickly becoming fast again is fine), I think my snippet with a
MutableState*Flow*
is fine and the shortest thing for my use case, isn't it?
My use case, BTW, is re-recording a
GraphicsLayer
after the app comes back visible from the background, as they seem to be evicted from the video memory and render empty if I don't make sure they are recorded again on coming back visible (started/resumed lifecycle state). That means missing when the value becomes true and becomes false very quickly is actually a nice feature to have, to avoid re-recording needlessly, though I don't think it'd really happen in real-life.
z
Ah. I wonder if @Nader Jawad has a better solution for this
From @Nader Jawad:
There were recent fixes that were landed around this
l
Any link to the CLs?
l
Thank you Nader!
👍 1
Some (all?) of those changes are only for the version after 1.7.0-beta01 (current), right?
Release next Wednesday? Matters to me because I'll talk about those APIs at KotlinConf on Thursday, and that's basically an immutable record (pun intended).