:mega: For all of you who picked :two: (using `Cha...
# coroutines
e
📣 For all of you who picked 2️⃣ (using
Channel
) how do you work around the fact that
Channel.receive
can get a message in an activity that is already destroyed and thus attempt to work with it will cause an
IllegalStateException
? (see this demo https://pl.kotl.in/Rs18kihuJ) The same problem happens when you use
Channel.receiveAsFlow
and then collect from the flow (see this demo https://pl.kotl.in/-ehZrPMl4). 😱 I never knew it could happen! All my code is broken! 👍 It never happens to me because I use
immediate
dispatcher (Android provides that by default) and I always post from the main thread, so between the event is posted and is being processed my activities cannot get destroyed. 👉 I do something else (comment in 🧵)
👍 7
😱 3
👉 4
s
ViewModels expose properties that are such 'event-flows'. Such a property is still a Flow and the Activity/Fragment uses
whenStarted
or
whenResumed
when collecting the Flow (uses a pausing dispatcher) https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.Lifecycle).whenStarted(kotlin.coroutines.SuspendFunction1)
b
similar to 👆 Channel/Flow can be collected using lifecycleScope.launchWhenStarted ([Resumed]) instead of someScope.launch. In this case coroutine won't be resumed if activity is in destroyed state
e
whenStarted
does prevent
IllegalStateException
for sure. But here is a problem with it. If you posted an event an activitity that was collecting using
whenStarted
and is now destroyed, then it will receive an event from the channel, but it will get cancelled before having a chance to process the event. Thus the event will be lost. However, this can only happen if you use non-immediate dispacher or post events from a background thread. If you are doing neither, then you don’t even need
whenStarted
in the first place.
s
In our case, loosing an ‘event-flow’ is no problem. The user left the app, they need not to go anywhere. If the app was backgrounded and an event was sent and
whenStarted/Resumed
is used, the navigation doesn’t happen, until the user foregrounds the app again. This is fine (like event-LiveData). We still need
whenStarted
or
whenResume
, because the nav-event should not happen while the activity is in the background.
Using
whenStarted
does not cancel the CoroutineScope when the activity moves to a STOPPED state. It pauses the dispatcher instead.
e
But when you rotate a screen, for example, your activity’s lifecycle is not just stopped. It goes all the way to DESTROYED (which cancels its scope and all coroutines runining within) and then created again. That’s the case I’m talking about.
b
I think Roman meant the case when lifecycle is destroyed e.g. after device rotation
a
There's also the case of process recreation from saved instance state bundles which affects navigation patterns
s
It is not destroyed. IIRC, The scope used is the
viewModelScope
and
whenStarted
is attaching a pausing dispatcher (
withContext(PausingDispacther())
). When rotating the device, the
viewModelScope
is not canceled.
a
If you have operations on incoming events modifying state, and observers of that state constructing/updating the UI from that state in an idempotent way, then these problems tend to melt away. The observers that work with lifecycle-sensitive operations based on observable application state can be safely cancelled if the lifecycle state is less than STARTED and re-launched later, and you have to implement this code path to implement saved state restoration anyway.
e
Ok. If you receive events in
viewModelScope
(that is not cancelled) using
whenStarted { … }
then it should be fine. The event you received will wait until the activity is restarted.
@Adam Powell That points more to 1️⃣, I think. E.g. if I have an event “show this message once”, then whether event was shown or not becomes a part of your state and that is what an
Event
wrapper object does for you — makes it a part of your state. Or do you have some other solution in mind here?
s
Yes, you have to be careful to use the
viewModelScope
and not the lifecycle-scope of the activity/fragment.
a
@elizarov I'll write up some more thoughts once I have had some this morning and can articulate them a bit better 🙂 there are similarities but generalizing it as an
Event
wrapper instead of reducing it to a domain-specific state causes some difficulties. The example from the article kind of works because
LiveData
is always conflated, and it still doesn't handle the queue of messages to be addressed by the user very well
e
@Adam Powell Thanks. Looking forward to it. To give you some more background on why I’m asking is that we are looking at future design directions for coroutines library. Clearly, unlike
LiveData
, coroutines have a rich set of tools that go beyond representing state (you have channels for one, that are directly designed for events), so you don’t have to take a solution designed to represent state and work around it (even though you can). What I’m trying to figure out is whether existing tools we provide are clear and can be used to solve the problem in a concise way or if we need to be adding something/anything else to make developers’ lives easier, and/or just give them more guidance.
a
and thank you! I wanted to jump in here because I think the
Event
wrapper pattern would be an unfortunate thing to carry into kotlinx.coroutines since I think there are better tools there already
in particular there are some great ways to involve user feedback into the backpressure handling
Some discussions I had with Jose recently led to this exploration (using compose's snapshot observable state type, but the example would work equally well with a MutableStateFlow) https://gist.github.com/adamp/47f2762d70bc33a5e2c75a31413ec202 - as an example it still makes assumptions around state saving responsibility but for the specific use case it's arguable whether it needs to save instance state or whether the kinds of conditions indicated by a snackbar notification widget should always be acting on live conditions anyway.
one useful property of this arrangement of the state is that since the
Mutex
forms a queue of messages for the user to address, if a condition that something wanted to notify the user about is no longer relevant, simply cancelling the call to
serve
removes it, or causes it to disappear if it's currently being shown.
and that lets the UI layer apply policies that it has enough information for, such as timing out a message if the user has seen it for X seconds, etc, while still permitting multiple UIs to present information from the same state object without issue.
l
For optimal UX, that'd involve counting the time spent in UI. In other words, a
delay
aletrnative that pauses when lifecycle is not
resumed
(or
started
?)
👍 1
a
@louiscad yes exactly, or cancelling the delay and restarting it if the policy is that the user must see it for X uninterrupted seconds
👍 1
the neat thing here is that since the producer side of such an event is a suspend function that returns the user's response, you can lace it through as part of a
flow.retryWhen {}
block for handling errors in other streams
by contrast, generalizing an
Event
wrapper at best starts to hit a local maximum, it works when you assume conflated state endpoints and events that can be safely conflated in such a way without disrupting the end result
and many of the cases where that works are easier to think about as a sort of state reducer pattern; instead of conflating a bunch of mutable am-I-consumed wrapped "navigate to details screen for item 231" kind of events, reduce to a, "current screen is details screen for item 231" state and don't make the consumer think about the extra baggage
and even in the navigation case the state involves more than that anyway which already makes the event wrapper awkward, like any sort of current navigation stack rather than just current screen
once again the baggage existing Android components bring to this heavily influenced the medium post - things like the system's activity stack and the FragmentManager both want to be a source of truth for their current state and stack, they want to act on edge events rather than state, but we've slowly been moving to more of a world where app code owns those sources of truth and this is a place where the seams are showing in that transition
so instead we have app state syncing to things like FragmentManager state, since app state can continue to be updated by a reducer from other events outside of specific lifecycle bounds and apply relevant changes to the lifecycle-bound components when possible
ideally over time we can lift more of that out and let the app stay in charge of it
m
@elizarov Here's how I process the events https://gist.github.com/manueldidonna/37421658640ea273b94cea85ee8176a3 . I hope it is relevant to your request.
BaseScene
is just an abstraction over the activity lifecycle which has start/stop and attach/detach callbacks
BaseScene
survives to configuration changes and what I've called FakeViewImpl in the example should be a real inflated android view
t
I just use yield()