In this commit (<https://github.com/erikhuizinga/2...
# android
e
In this commit (https://github.com/erikhuizinga/2021-12-16-developer.android.com-jetpack-guide-ui-layer-events/commit/9503f634bea5d9f6ab971c6a4814c1944bf9db4c) I'm trying to observe the state (
StateFlow<State>
) in two fragments in the same activity. As can be seen in the logcat at runtime, only one of the two fragments receives new state values. I don't understand why, because a
StateFlow
should emit to all collectors, not just one. Why is that? What am I doing incorrectly?
g
You observe only onStart for both fragments, so you have both fragments visible at the same time on the screen?
👌 1
So in general, I don't think it's an issue with StateFlow, you right, it will emit to all collectors, it more look like an issue who is collecting and from which flow
👍 1
It can be an issue with a lifecycle (so one of collectors already cancelled), or with VM and you just getting different VMs
e
Thanks @gildor! I have given it some more thought, but I'm not yet sure what is happening. Both fragments are visible (started) and collecting using the same
Main
dispatcher (possibly the
immediate
variant). This is a single threaded looper. Is it possible that the first fragment collects the state and consumes it (notifies view model) and the view model resets the state, all in the same main thread loop, while the second fragment's collector is suspended until a future loop is available? If so, then it doesn't surprise me how this behaves, because in the next loop the state is back to what it was before, and equal states aren't emitted by
StateFlow
. I'll have to try later if I can
post {}
the state consumption, or use a multi-threaded dispatcher, or a suspending function before consuming the event (e.g.
delay
).
g
Is it possible that the first fragment collects the state and consumes it
Consume event?
I’ll have to try later if I can 
post {}
 the state consumption, or use a multi-threaded dispatcher, or a suspending function before consuming the event (e.g. 
delay
). (edited)
If it somehow caused by immediate, just do not use immediate, use Dispatchers.Main explicitly Still I’m not quite understand the problem, if state is consumed and moved to another state, why is it bad? Because both fragments just should show some existing state
e
What's seemingly not working properly is the property of shared/state flows: all observers observe the emissions (hence: shared). But one of the two fragment doesn't receive the state updates.
I've now put the consumption of the event in
<http://view.post|view.post> { }
and now the logcat shows that both fragments collect all state updates. This confirms (but doesn't prove) my theory
And indeed, if I use a
delay(50)
(instead of
post
) before consuming the event, that allows the both fragments to collect all states too. Another confirmation
And indeed, if I wrap the event consumption in
withContext(Dispatchers.Default) { }
which is multi-threaded (and a suspending function, but that shouldn't matter here?), also both fragments observe all state updates
j
The problem is that you are resetting the value directly
the collecting is done with an immediate dispatcher, meaning if you are on the same thread it will execute directly
stateflows only have 1 value at a time
so apparently changing the value during emitting a new value will affect the collection
This is what you want: * updating state flow * collecting fragment 1 * collecting fragment 2 * reset value
this is what you get: * updating state flow * collecting fragment 1 -> block is directly run because of immediate dispatcher * reset value * collecting fragment 2 -> state flow didn't change from last time, nothing to collect
you should look into event flows
they are not a thing 😄 but they are flows constructed to handle event like types that are only consumed once
We have events handled like this, I adjusted to use the repeatOnLifecycle, so perhaps this doesn't work but you can try it out:
Copy code
private val eventChannel = Channel<LoginContract.Event>(Channel.BUFFERED)

	override val events = eventChannel.receiveAsFlow()

// collecting
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.events.collect { event ->
            // do something here
        }
    }
new events you push to the eventChannel
events were the biggest flaw in ViewModel with Livedata, but with flows it should be possible
e
Thanks Joost! The problem with channels is that events might be undelivered if you're not careful (see the comments in this gist: https://gist.github.com/gmk57/330a7d214f5d710811c6b5ce27ceedaa) Also, the reason I put the 'events' in the UI state is that the official Android documentation was recently updated with this recommendation (see https://developer.android.com/jetpack/guide/ui-layer/events), for various reasons. So, I was trying to observe event-like data in the UI state in more than one fragment, to see how the Android recommendations hold in various practical situations. I then discovered the surprising (to me) behaviour that a state flow collector doesn't observe all state values even if the state changed! This seems to contradict the state flow contract (or the contract isn't correct/complete) that all collectors receive changed values.
j
it does mention there it is mainly when you are firing from different threads the events, which I would guess is usually not the case.
viewmodelscope is also main thread
but I am still wondering about this statement from your last message
I then discovered the surprising (to me) behaviour that a state flow collector doesn't observe all state values even if the state changed!
in your example earlier, state flow seems to uphold its contract
it will only emit something if the value has changed
so if in the collect#1 you reset the value as how it was before the change, then in collect#2 there technically was no change
e
I expected the second collector to also collect both changes, that's my surprise
Like you said: it should be common for these state updates to happen synchronously on the immediate main dispatcher. That means that a programmer might think that all observers would observe all changes, but if the state resets at the end of the same main thread loop, then other observers on that thread won't observe any changes before the reset
This might lead to bugs in your applications if you're not aware of this.
It's a side effect to be aware of when following the Android UI event guidelines
j
yeah I guess it can bite you in the ass 🙂 It is about state and not about events. I guess that is the main misconception here, if the state didn't change (because you reset it during collection somewhere else), there is no need to update anything. If you look into StateFlowImpl you can see inside
collect
this part what actually shows the behavior:
Copy code
if (oldState == null || oldState != newState) {
    collector.emit(NULL.unbox(newState))
    oldState = newState
}
This case will happen if you change the state while you collect in an Unconfined or Immediate dispatcher Also, I did check the thread in your link and the example about undelivered items, and it was interesting to see that in that specific case it was reproducable to undeliver events 🙂 but when I made all the coroutines fire from the same context I couldn't reproduce, and in viewmodel + view world that will normally be the case.
But anyway, I think with view states in practice you should be in the clear: They are handled on the main thread and I would not expect them to be shared across fragments or collected in different parts of the app. As well as with Jetpack compose there will be much more finegrained components, meaning less god-like states, and what in the "old" view world would be necessary to fire as an event (Toast, alert dialog, etc.) in compose should be a state. So in compose if something like a snackbar should show for 5 seconds, then most likely you will launch a coroutine somewhere which after 5 seconds updates the view state to remove the snackbar.