Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

viewModelScope not cancelled

After watching Sean's explanation on Android (Google I/O'19) I've tried the same:

init{
    viewModelScope.launch {
        Timber.i("coroutine awake")
        while (true){
            delay(2_000)
            Timber.i("another round trip")
        }
    }
}

Unfortunately onCleared it's called when the activity is killed but not when it's put in background ("when we move away from the Activity...", background is "moving away" imho ^^).
And I get the following output:

> ---- Activity in Foreground
> 12:41:10.195  TEST: coroutine awake
> 12:41:12.215  TEST: another round trip
> 12:41:14.231  TEST: another round trip
> 12:41:16.245  TEST: another round trip
> 12:41:18.259  TEST: another round trip
> 12:41:20.270  TEST: another round trip
> ----- Activity in Background (on onCleared not fired)
> 12:41:22.283  TEST: another round trip
> 12:41:24.303  TEST: another round trip
> 12:41:26.320  TEST: another round trip
> 12:41:28.353  TEST: another round trip
> 12:41:30.361  TEST: another round trip
> ----- Activity in Foreground
> 12:41:30.369  TEST: coroutine awake

How can I solve this?

1 - Move the code from init to a suspend fun start() called by the activity inside a lifecycleScope.launchWhenStarted?

I get the same result. I thought lifecycleScope would cancel its child coroutines when it went to background, but I get the same Timber output with this approach.

2 - Change my ViewModel code to:

private lateinit var job: Job

suspend fun startEmitting() {
    job = viewModelScope.launch {
        Timber.i("coroutine awake")
        while (true){
            delay(2_000)
            Timber.i("another round trip")
        }
    }
}
fun cancelJob(){
    if(job.isActive){
        job.cancel()
    }
}

And, in my Activity:

override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
        viewModel.startEmitting()
    }
}
override fun onPause() {
    super.onPause()
    viewModel.cancelJob()
}

Well it works but isn't the viewModelScope purpose to manage the CoroutineScope for me? I hate this cancelJob logic.

What's the best approach to deal with this?

like image 359
GuilhE Avatar asked Sep 25 '19 13:09

GuilhE


People also ask

How do I turn off ViewModel scope?

In the clear() method the ViewModel cancels the Job of the viewModelScope.

What is ViewModelScope in Android?

A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel is active.

Is ViewModelScope on main thread?

ViewModelScope. launch { } runs on the main thread, but also gives you the option to run other dispatchers, so you can have UI & Background operations running synchronously.

What is not Hardcode dispatchers?

Don't hardcode Dispatchers when creating new coroutines or calling withContext . This dependency injection pattern makes testing easier as you can replace those dispatchers in unit and instrumentation tests with a test dispatcher to make your tests more deterministic.

What is viewmodelscope in Android lifecycle?

AndroidX lifecycle v2.1.0 introduced the extension property viewModelScope to the ViewModel class. It manages the coroutines in the same way we were doing in the previous section.

What happens when the ViewModel is cleared?

When the ViewModel is cleared, it executes the method clear () before calling the onCleared () method that we would’ve had to override otherwise. In the clear () method the ViewModel cancels the Job of the viewModelScope. The full ViewModel code is also available but we are just focusing on the parts we are interested in:

How to close the scope of a ViewModel in Java?

In order for the ViewModel to close the scope, it needs to implement the Closeable interface. That’s why viewModelScope is of type CloseableCoroutineScope that extends CoroutineScope overriding the coroutineContext and implements the Closeable interface.

What does it mean to cancel a scope?

A CoroutineScope keeps track of all coroutines it creates. Therefore, if you cancel a scope, you cancel all coroutines it created. This is particularly important if you’re running coroutines in a ViewModel. If your ViewModel is getting destroyed, all the asynchronous work that it might be doing must be stopped.


2 Answers

Kotlin can't cancel an infinite operation for you. You need to call isActive somewhere. For example: while(isActive).

like image 112
Jose Alcérreca Avatar answered Sep 22 '22 02:09

Jose Alcérreca


You can write your own LiveData class instead of using MutableLiveData.

When you do that you can override methods that explicitly notify you if there are any active listeners.

class MyViewModel : ViewModel(){
    val liveData = CustomLiveData(viewModelScope)

    class CustomLiveData(val scope) : LiveData<Int>(){
        private counter = 0
        private lateinit var job: Job

        override fun onActive() {
            job = scope.launch {
                Timber.i("coroutine awake")
                while (true){
                    delay(2_000)
                    Timber.i("another round trip")
                    postValue(counter++)
                }
        }

        override fun onInactive() {
            job.cancel()
        }
    }
}

Then inside your Activity you no longer need to explicitly call any start/pause as LiveData will automatically begin and cancel coroutine depending on observers state.

like image 44
Pawel Avatar answered Sep 25 '22 02:09

Pawel