So I have this weird problem where a flow’s collec...
# coroutines
a
So I have this weird problem where a flow’s collector is called when the scope is cancelled. I added some logging to confirm this is the case:
Copy code
val flow = repository
        .observeStuff(id)
        .map { it.map { it.createViewModel() }}
        .combine(otherFlow) { stuff, otherStuff -> stuff to otherStuff }
        // Don't emit until the settings are loaded
        .onEach { settingsLoading.join() }
        .conflate()
        .flowOn(Dispatchers.Default)
        .broadcastIn(viewModelScope, start = CoroutineStart.DEFAULT)
        .asFlow()

scope.launch {
  flow.collect {
    // Rare crash because the scope is cancelled and the fragment is destroyed
    view.update(it)
  }
}
It really feels like it’s a bug in the coroutines framework because I can’t explain it otherwise I can fix it by adding a
yield
or
if (coroutineContext[Job]?.isCancelled == true) throw CancellationException()
in my
collect
but I really want to figure out what is happening. I’ve only seen it in Crashlytics I added some logging to crashlytics to confirm the scope is cancelled:
Copy code
CrashReporting.log("isStarted: %b, view == null: %b, isCancelled: %b".format(isStarted, view == null, coroutineContext[Job]!!.isCancelled))
And in the crash report is says:
Copy code
isStarted: false, view == null: true, isCancelled: true
o
flow.collect
doesn't check for cancellation, and neither does most of the Flow methods (perhaps
asFlow
would simply due to it using a channel underneath), so you must check for it manually
a
Yeah, I know. It’s because they all run in the same context but since the flow scope is on the Android main thread there can’t be any race conditions
Cancellation also happens on the main thread
Something in the channel flow is allowing emissions even though the scope is cancelled
The broadcast channel scope is probably not canceled though
s
Is
viewModelScope
the same instance as
scope
? And, what is the stack-trace of the crash/exception you see?
a
No, the view model scope lives longer than the other scope, both are using the main thread dispatcher though. The crash is because I’m accessing the fragment view which is not there because the fragment is destroyed
But I don’t think the broadcast channel’s scope should matter really
s
Yeah... even if they are different (and they are), the
asFlow()
should take care of that, 'un-subscribing' the flow it returns from the BroadcastChannel (returned by broadcastIn), when
scope
is cancelled. Maybe a bug...? At worst, your BroadcastChannel may suspend forever or memory issues (unlimited buffer), not the collect that keeps going after its
scope
gets cancelled. Just to be sure, have you checked that the
scope
in your code is tied properly to that Fragment being destroyed. Also, be sure to that your Fragment's 'CoroutineScope' is tied to its
viewLifecycleOwner
, not to its
lifecycleOwner
...
a
Yeah, I’ve verified that when I get the crash the scope is in fact cancelled
The channel is conflated so hopefully the memory isn’t an issue. The lifecycle scope should either be canceled shortly after or another fragment will be created and consume the flow
s
Did you tie
scope
to the Fragment's
lifecycleOwner
or its
viewLifecycleOwner
?
a
Neither, it’s a scope that is created in
onStart
and cancelled in
onStop
s
I assume the
onStart
and
onStop
of the Fragment :) That should be working.... the
view
of a Fragment doesn't get destroyed/recreated while in 'started' state.....
a
Nope, I’ve confirmed that fragment is stopped, the view is destroyed and the scope is cancelled (see the logging)