I have a StateFlow coroutine that is shared amongst various parts of my application. When I cancel the CoroutineScope of a downstream collector, a JobCancellationException is propagated up to the StateFlow, and it stops emitting values for all current and future collectors.
The StateFlow:
val songsRelay: Flow<List<Song>> by lazy {
MutableStateFlow<List<Song>?>(null).apply {
CoroutineScope(Dispatchers.IO)
.launch { songDataDao.getAll().distinctUntilChanged().collect { value = it } }
}.filterNotNull()
}
A typical 'presenter' in my code implements the following base class:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val job by lazy {
Job()
}
private val coroutineScope by lazy { CoroutineScope( job + Dispatchers.Main) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
job.cancel()
view = null
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block)
}
}
A BasePresenter implementation might call launch{ songsRelay.collect {...} }
When the presenter is unbound, in order to prevent leaks, I cancel the parent job. Any time a presenter that was collecting the songsRelay StateFlow is unbound, the StateFlow is essentially terminated with a JobCancellationException, and no other collectors/presenters can collect values from it.
I've noticed that I can call job.cancelChildren() instead, and this seems to work (StateFlow doesn't complete with a JobCancellationException). But then I wonder what the point is of declaring a parent job, if I can't cancel the job itself. I could just remove job altogether, and call coroutineScope.coroutineContext.cancelChildren() to the same effect.
If I do just call job.cancelChildren(), is that sufficient? I feel like by not calling coroutineScope.cancel(), or job.cancel(), I may not be correctly or completely cleaning up the tasks that I have kicked off.
I also don't understand why the JobCancellationException is propagated up the hierarchy when job.cancel() is called. Isn't job the 'parent' here? Why does cancelling it affect my StateFlow?