Tim Malseed
07/04/2020, 2:07 PMStateFlow
to sort of cache and share the result of a coroutine. The StateFlow
is held in a singleton, and accessed (collected) from multiple different places.
When the scope that launches one of these collectors is cancelled (via scope.cancel()
, it seems that the StateFlow
is cancelled and no longer emits to any collectors.
Can I cancel the collectors launched in a scope, without cancelling the parent producer?Dominaezzz
07/04/2020, 2:16 PMTim Malseed
07/04/2020, 2:20 PMJob
to the launch
call, and cancel that job rather than calling scope.cancel
Tim Malseed
07/04/2020, 2:25 PMDominaezzz
07/04/2020, 2:25 PMcancel
the returned Job
.louiscad
07/04/2020, 2:26 PMcollect
, not the coroutine that emits to the MutableStateFlow
.Tim Malseed
07/04/2020, 2:27 PMcoroutineScope.cancel()
, thinking it would do just thatTim Malseed
07/04/2020, 2:29 PMPresenter
class:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val coroutineScope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
}
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
view = null
coroutineScope.cancel()
}
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(context + exceptionHandler, start, block)
}
}
Tim Malseed
07/04/2020, 2:30 PMcoroutineScope.launch()
. and when I’m done with this presenter, I call unbind()
, which calls coroutineScope.cancel()
Tim Malseed
07/04/2020, 2:31 PMTim Malseed
07/04/2020, 2:35 PMlouiscad
07/04/2020, 2:36 PMlouiscad
07/04/2020, 2:36 PMTim Malseed
07/04/2020, 2:40 PMval parentJob = Job()
scope.launch(parentJob) { coroutine.collect... }
...
parentJob.cancel()
This didn’t seem to work (I still get the sense that the cancellation exception is propagating upstream). Is this different to what you’re suggesting?louiscad
07/04/2020, 2:42 PMval job = launch {
launch {
someFlow.collect { element -> ... }
}
launch { anotherFlow.collect { -> ... } }
}
// later on
job.cancel()
Tim Malseed
07/04/2020, 2:45 PMTim Malseed
07/04/2020, 2:45 PMTim Malseed
07/04/2020, 2:51 PMprentJob
thing does work,. You just need to call cancelChildren()
instead of cancel
Tim Malseed
07/04/2020, 2:52 PMabstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val coroutineScope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)
private val parent = Job()
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
}
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
view = null
parent.cancelChildren()
}
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(parent + context + exceptionHandler, start, block)
}
}
louiscad
07/04/2020, 2:54 PMTim Malseed
07/04/2020, 2:57 PMcancelChildren()
does seem to solve my immediate problem, and feels like a neater approach - but I absolutely need to learn more about this - thanksTim Malseed
07/04/2020, 3:04 PMcancel()
on the CoroutineScope
nowDominaezzz
07/04/2020, 3:05 PMGlobalScope
in that case.Tim Malseed
07/04/2020, 3:07 PMlouiscad
07/04/2020, 3:07 PMTim Malseed
07/04/2020, 3:09 PMStateFlow
coroutine, and that is by design. There’s no child coroutine that keeps running that might emit and try to manipulate a view after unbind is called (I think)Tim Malseed
07/04/2020, 3:10 PMlouiscad
07/04/2020, 3:10 PMlouiscad
07/04/2020, 3:10 PMLifecycleOwner
Tim Malseed
07/04/2020, 3:11 PMTim Malseed
07/04/2020, 3:12 PMparentJob.cancelChildren()
and then call coroutineScope.cancel()
..?Tim Malseed
07/04/2020, 3:13 PMTim Malseed
07/04/2020, 3:14 PMBasePresenter
now looks like this, and seems to behave as expected:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val coroutineScope by lazy { CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) }
private val parent by lazy { Job() }
private val exceptionHandler by lazy {
CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
}
}
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
view = null
parent.cancelChildren()
coroutineScope.cancel()
}
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(parent + context + exceptionHandler, start, block)
}
}
Tim Malseed
07/04/2020, 3:16 PMlaunch()
is launched with parent
as its parent Job
, so the coroutine is cancelled in unbindView()
, without propagating the cancellation exception to related/parent coroutines. And then the entire scope is cancelled ,just to be sure.Tim Malseed
07/04/2020, 3:17 PMlouiscad
07/04/2020, 3:21 PMTim Malseed
07/04/2020, 3:22 PMlouiscad
07/04/2020, 3:23 PMildar.i [Android]
07/04/2020, 4:22 PMCoroutineScope(parent + <http://Dispatchers.IO|Dispatchers.IO>)
that way you bind them and don't need to manually cancel your scopeTim Malseed
07/05/2020, 11:38 AMparent + <http://Dispatchers.IO|Dispatchers.IO>
, then when the parent.cancelChildren()
is called, the CoroutineScope
will be automatically cancelled?ildar.i [Android]
07/05/2020, 12:00 PMparent.cancelChildren()
won't cancel its CoroutineScope
, it will cancel all child scopes. To cancel parent you should call parent.cancel()
, but you can do it only once - if you return to presenter with cancelled scope, it won't work anymore. You can read these articles: https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055 and https://proandroiddev.com/why-your-class-probably-shouldnt-implement-coroutinescope-eb34f722e510
For Android ViewModel there is a viewModelScope
, but since you have a presenter you should stick to manually creating your scopeTim Malseed
07/05/2020, 12:04 PMCoroutineScope(parent + <http://Dispatchers.IO|Dispatchers.IO>)
. For my use case, calling parent.cancelChildren()
, followed by coroutineScope.cancel()
, seems to do the trick. I’m not sure what the benefit or difference is of the approach you’re suggestinglouiscad
07/05/2020, 12:09 PMparent.cancel()
, in place of parent.cancelChildren()
, and coroutineScope.cancel()
. It should work the same.ildar.i [Android]
07/05/2020, 12:09 PMTim Malseed
07/05/2020, 12:09 PMCancellationException
to propagate up the Coroutine hierarchy, which is what I’m trying to avoidTim Malseed
07/05/2020, 12:12 PMpresenter.unbind()
, when the presenter will never be used again - so the fact that the scope is cancelled and unusable isn’t a problem.
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
private val parentJob by lazy { Job() }
private val coroutineScope by lazy { CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
parentJob.cancelChildren()
coroutineScope.cancel()
}
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(parentJob + context, start, block)
}
}
This seems to work. But I’m not really sure if there’s any real point in calling coroutineScope.cancel()
, since there are no child jobs at this point anyway.Tim Malseed
07/05/2020, 12:13 PMparentJob.cancelChildren()
, then I avoid a CancellationException
. Then I can call coroutineScope.cancel()
, and still avoid the CancellationException
, and everything seems fine. But, It feels a little messy.Tim Malseed
07/05/2020, 12:15 PMildar.i [Android]
07/05/2020, 12:16 PMildar.i [Android]
07/05/2020, 12:17 PMTim Malseed
07/05/2020, 12:18 PMlaunch()
- so from within a Presenter you can call launch and you get an exception handler and a parent job that can cancel its children, for free.ildar.i [Android]
07/05/2020, 12:18 PMTim Malseed
07/05/2020, 12:20 PMcoroutineScope.launch()
, so, are they ‘separate’? Can you elaborate on this?Tim Malseed
07/05/2020, 12:22 PMildar.i [Android]
07/05/2020, 12:22 PMTim Malseed
07/05/2020, 12:22 PMTim Malseed
07/05/2020, 12:23 PMlaunch {
coroutine.collect { ... }
}
ildar.i [Android]
07/05/2020, 12:23 PMTim Malseed
07/05/2020, 12:24 PMfun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(parentJob + context + exceptionHandler, start, block)
}
ildar.i [Android]
07/05/2020, 12:25 PMTim Malseed
07/05/2020, 12:26 PMparentJob
and ?ildar.i [Android]
07/05/2020, 12:26 PMTim Malseed
07/05/2020, 12:28 PMTim Malseed
07/05/2020, 12:31 PMprivate val coroutineScope by lazy { CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>) }
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return coroutineScope.launch(parentJob + context + exceptionHandler, start, block)
}
I can see that perhaps one of the problems here, is that coroutineScope
is initialised with a context of <http://Dispatchers.IO|Dispatchers.IO>
:
But then coroutineScope.launch
is called, and a new context is passed, comprised of parentJob + context + exceptionHandler
- so the `Dispatchers.IO`` context is potentially discarded / redundant?Tim Malseed
07/05/2020, 12:37 PMabstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val parentJob by lazy { Job() }
private val exceptionHandler by lazy {
CoroutineExceptionHandler { _, exception -> Timber.e(exception) }
}
private val coroutineScope by lazy { CoroutineScope(parentJob + exceptionHandler + <http://Dispatchers.IO|Dispatchers.IO>) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
parentJob.cancelChildren()
view = null
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block)
}
}
ildar.i [Android]
07/05/2020, 12:40 PMildar.i [Android]
07/05/2020, 12:40 PMTim Malseed
07/05/2020, 12:41 PMTim Malseed
07/05/2020, 12:41 PMildar.i [Android]
07/05/2020, 12:42 PMTim Malseed
07/05/2020, 12:43 PMildar.i [Android]
07/05/2020, 12:44 PMTim Malseed
07/05/2020, 12:45 PMTim Malseed
07/05/2020, 12:47 PMTim Malseed
07/05/2020, 12:47 PMTim Malseed
07/05/2020, 12:48 PMcoroutineScope
itself is never cancelled. Only the children of parentJob
. I mean, I think that’s fine, and it means there won’t be any coroutines running inside of the presenter after unbind is called, but something about not calling cancel
on the scope that makes me wonder if this is.. completeildar.i [Android]
07/05/2020, 12:49 PMTim Malseed
07/05/2020, 12:49 PMTim Malseed
07/05/2020, 12:51 PMparentJob
altogether, and just call `coroutineScope.coroutineContext.cancelChildren()`:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val exceptionHandler by lazy {
CoroutineExceptionHandler { _, exception -> Timber.e(exception) }
}
private val coroutineScope by lazy { CoroutineScope(exceptionHandler + <http://Dispatchers.IO|Dispatchers.IO>) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
coroutineScope.coroutineContext.cancelChildren()
view = null
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block)
}
}
Tim Malseed
07/05/2020, 1:33 PMildar.i [Android]
07/05/2020, 1:40 PMTim Malseed
07/05/2020, 1:47 PMTim Malseed
07/06/2020, 2:57 AM