For what reason does snapshotFlow() mix the concep...
# compose
u
For what reason does snapshotFlow() mix the concept of state updates with the concept of equality? I can easily say snapshotFlow().distinctUntilChanged() if I really want to check equality, but in the present system I cannot easily observe state updates where the evaluation result does not change in terms of equality, e.g. with SnapshotStateList (would have to make explicit list copies). Seems poorly thought out.
f
State represents the UI, if the state doesn't change there's nothing to recompose. Looks like you may want to have events instead of state.
u
I don't believe recomposition tests for equality, it merely observes state change.
f
state changes mean the state you have now does not equal the state you had before
u
snapshotFlow { mySnapShotStateList }
evaluates its lambda block upon a state change of
mySnapshotStateList
, which occurs whenever there is a list update, but it does not emit, because the list object remains the same, and is equal to itself, of course.
a
It's very deliberate. If the object is equal to the previous value, output state hasn't changed. snapshotFlow emits state derived from snapshot state, not change events
if you'd like to post the code for the use case that is giving you trouble, one of us might be able to suggest another approach
u
When I have
val intState = mutableStateOf(1)
and over time change state to 2, then 3,
snapshotFlow { intState.value }
may only emit 1 and 3, or 2 and 3, or just 3. This is how I understand the difference between change events and snapshot state.
When I have
val snapshotStateList = mutableStateListOf<Int>()
and keep adding elements to the list, then
snapshotFlow { snapshotStateList }
will never emit more than once, even though snapshot state keeps changing. This behavior is not useful.
When I have
val intState = mutableStateOf(1)
and over time keep "updating" the state with 1,
snapshotFlow { intState.value }
does nothing at all, after the initial emit, because snapshot state never actually changes.
It's not clear to me how the additional equals check in
snapshotFlow
relates to change events. It just seems like an additional filter that a flow consumer could choose to add or not add.
Is the reason for the equals check the case where, in the first example, if
intState
changes back to 1, after 3,
snapshotFlow
without an equals check could end up emitting a sequence of '1', '1', (the state object is cycling through 1,2,3,1) which does not make sense when one expects state output.
Which makes sense only if
snapshotFlow
is intended to emit something that behaves like a state, even though the output is not actually a state, just some derivative. Seems weird.
derivedStateOf
as a flow I suppose?
a
When I have
val snapshotStateList = mutableStateListOf<Int>()
and keep adding elements to the list, then
snapshotFlow { snapshotStateList }
will never emit more than once, even though snapshot state keeps changing.
snapshotStateList
is just a reference to an object. It only emits once because there is only ever one thing to emit: the reference to the single list. This
snapshotFlow
hasn't accessed any snapshot state to observe new values of
it seems like this thread is conflating two different things, because unless you've otherwise accessed the contents of the list in the
snapshotFlow
block, there are no reads of snapshot objects to monitor for changes
or as another example, if you had the following code:
Copy code
val inner = MutableStateFlow(1)
val outer = MutableStateFlow(inner)
launch {
  outer.collect {
    println(it.value)
  }
}
inner.value = 2
outer.value = inner
the launched collector wouldn't see a change either
u
Yes, the single reference in snapshotFlow has thrown me off before (snapshotFlow { state } does not work, without accessing the value property). Thank you!
snapshotFlow
keeps confusing me in this fashion.
a
regarding the
distinctUntilChanged
-style behavior, two examples of why doing otherwise for the example would be undesirable:
snapshotFlow {}
maintains a consistent snapshot within its block where all snapshot reads see an atomically consistent state across objects. As soon as you emit another snapshot state object from it, those objects are no longer guaranteed to hold the same values they did when the block was running. Depending on buffering, dispatch, and other flow operator behavior, the point where the emit runs no longer correlates to a single atomic state of the data that represents when
snapshotFlow
noticed a change
an observer may very well see two emits in a row where the snapshot container holds the same values; (the first call to the observer might not run until after the second update to the emitted container has occurred) you've still lost any sort of intermediates, which means you still need to implement the observer in an idempotent manner. It then is a state observer that doesn't benefit from doing the same work for the same values repeatedly.
u
Thanks. My general expectation when seeing `someFlowGenerator { creatingabunchofobjects }.collect()' is that the generator runs in the context of my collector and produces elements without intermediate buffering. It is possible to violate this expectation, but I generally expect this to be documented, which I don't see. Because snapshotFlow accesses snapshot state I somewhat assumed that it would indeed work without dispatcher changes and buffering (hence causing a further loss of intermediate snapshot states). Because of how snapshot state works I expected the loss of intermediate states ("events"), but not dispatcher/buffering stuff.
a
it does run in the context of the collector, but it emits after the block returns and exiting the associated snapshot
u
It seems like a lost opportunity (and confusing) that snapshotFlow does not actually give me access to the snapshot state. It seems as if this could be done in a flow operator.
a
snapshotFlow does not actually give me access to the snapshot state
I'm not sure what you mean by this
u
Not the raw snapshot state, but the snapshotted state of the objects that I'm interested in
Like the snapshotted state of my SnapshotStateList
a
it does, but you're meant to access those things and emit your desired result from the
snapshotFlow
block rather than try to process the snapshot state objects themselves downstream.
snapshotFlow
is a translation edge between snapshots => flows
u
Which is not possible in the case of SnapshotStateList without creating an explicit list copy I think. (Or doing all my processing in the snapshotFlow block itself, which seems just awful)
I've rewritten this code with PersistentList directly, but I lose the less error-prone conveniences of SnapshotStateList
a
I'm still curious what your specific code looks like. Doing the processing in the
snapshotFlow
block itself may not be as prohibitive as it seems. As for the copy, unfortunately since kotlin's
Iterable.toList()
is an extension and not something we could override in
SnapshotStateList
itself we can't really hand over the underlying persistent list at that snapshot
(at least not in a manner that wouldn't miss expectations as soon as you're using it through a
[Mutable]List
reference)
u
The list is processed and modified by 3rd party legacy code, which I cannot make composable. The snapshotFlow ties everything together.
It's possible to do the processing in the snapshotFlow block, but then I'm running all the closed-source legacy code in a kind of composable context whose nature is not that well-defined and unchanging, so it's just pretty ugly. I want this code to run in a collector that gives me control over my execution context and not throw those different types of code together into a single place.
MutableState<PersistentList> works with snapshotStateFlow in a normal manner and should be as efficient as SnapshotStateList, but more error-prone due to reassignment syntax needs. My preference would be to have a kind of snapshotFlow where I have direct access to my State object. Oh well. Thank you for elaborating so extensively!
a
where's the composable context coming from? if you
snapshotFlow {}.flowOn(...)
you'll get the same kinds of results you would anywhere else. Snapshots and
snapshotFlow
aren't dependent on composition in any way
(which is to say the above would cause the
snapshotFlow
block to run wherever you
flowOn
to)
u
Ok, good to know! Still might be hard to tolerate by anyone who's not a die-hard Compose expert and has been following this thread 😉
a
¯\_(ツ)_/¯
happy to help, regardless 🙂
u
It was very helpful, thank you so much!
That snapshotFlow with MutableState<(Persisten)List> or SnapshotStateList is actually very useful, because it let me split my legacy code into 2 separate pieces which I was commingling before and I can tell one about the other's updates. And tying it all up in my Compose hierarchy, so I can l observe my updates from elsewhere.
Which would have been possible via flows, but definitely easier with Compose.
@Adam Powell One more thing, sorry: couldn't SnapshotStateList get a method or property added to extract the current PersistentList state as a Kotlin
List
?
List
can be anything, so having it be a private PersistentList implementation underneath seems alright to me. You said that a
toList()
extension function would not be feasible, but is there an issue with adding a custom method to SnapshotStateList like this? If
PersistentList
ever goes stable then the API could be changed around a bit, deprecating the original custom method/property. I can add something in the issuetracker if you think this could possibly see the light of day. This would make it usable in a similar way as
MutableState<List>
.
a
Go ahead and file a feature request for it, we'll need to make sure there aren't other gotchas to consider around it but it sounds like a good idea in the context of a slack conversation 🙂
🙂 1
👍 1
u
206 Views