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.cancelTim 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 cancelTim 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 PMLifecycleOwnerTim 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