Why does emit() cancel this coroutine?

This code works, I see a toast with “ping” every 5 seconds:

class DatesViewModel(private val appl: Application) : AndroidViewModel(appl) {
    val dates: LiveData<String> = liveData {
        viewModelScope.launch {
            try {
                while(true) {
                    delay(5_000)
                    Toast.makeText(appl, "ping", Toast.LENGTH_SHORT).show()
                }
            } catch (e: Exception) {
                Log.d("E", e.toString())
            }
        }
        emit("foo")
    }
}

But this does not:

class DatesViewModel(private val appl: Application) : AndroidViewModel(appl) {
    val dates: LiveData<String> = liveData {
        viewModelScope.launch {
            try {
                while(true) {
                    delay(5_000)
                    emit("bar") // This is the only changed line
                    Toast.makeText(appl, "ping", Toast.LENGTH_SHORT).show()
                }
            } catch (e: Exception) {
                Log.d("E", e.toString())
            }
        }
        emit("foo")
    }
}

The value “bar” is never actually emitted. I don’t see a single “ping” toast. The Log line logs:

D/E: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine has completed normally; job=StandaloneCoroutine{Completed}@c3e4ddf

Why does this happen and how shall I prevent it?

I was expecting the coroutine to be cancelled only when the ViewModel is cleared.

I think I know what is going on. This is the source of emit():

    override suspend fun emit(value: T) = withContext(coroutineContext) {
        target.clearSource()
        target.value = value
    }

My guess is that coroutineContext was cancelled (ended) when the liveData { ... } block ended, so trying to run something withContext(coroutineContext) throws a JobCancellationException?

I don’t know how to make it work though. emit() seems to be the only way to publish a new value to target.

The solution was so simple it hurts. Why launch another coroutine and let the first one end. Just keep the first one alive:

class DatesViewModel(private val appl: Application) : AndroidViewModel(appl) {
    val dates: LiveData<String> = liveData {
        emit("foo")
        while(true) {
            delay(5_000)
            emit("bar")
            Toast.makeText(appl, "ping", Toast.LENGTH_SHORT).show()
        }
    }
}

Another alternative might have been to create another LiveData or Flow, emit that with emitSource and then update that one.

It was this answer that made me think of the solution.

Why do you launch() a new coroutine using viewModelScope? I’m not 100% sure what is going on here, but it doesn’t seem right. By doing this the coroutine that is used to emit() to the flow does not know there is another coroutine that tries to emit. After emitting “foo” the coroutine finishes and the flow itself is also considered completed. Then “bar” can’t be emitted.

You were 3 minutes too late but spot on. :slight_smile:

Ok, I just found out that this is not the solution, at least not like this.

5 seconds after the last observer of the LiveData goes away, the coroutine is cancelled and then restarted when another observer comes. So even though the ViewModel’s Lifecycle didn’t end, this code essentially discards the data and starts a fresh recalculation every time the observer goes away for more than 5 seconds. This is not what I want.

That’s how liveData builder is designed to work so it doesn’t use resources while no one is listening. Just create your LiveData directly and launch with the lifetime you want.

class DatesViewModel(private val appl: Application) : AndroidViewModel(appl) {
    val dates: LiveData<String> = MutableLiveData(“foo”).also {
        viewModelScope.launch {
            while(true) {
                delay(5_000)
                it.setValue("bar")
                Toast.makeText(appl, "ping", Toast.LENGTH_SHORT).show()
            }
        }
    }
}
1 Like

Works perfectly.

Well, actually not perfectly perfectly… I didn’t realize that at first.

It would be better if the updating would indeed be cancelled, but the last data would be held.

So as long as there are observers, I see the “ping” toasts and “bar” is emitted. When the observer goes away, the “ping” toasts stop. But when the observer comes back, it immediately sees “bar”.

My solution fulfilled only the first part (the “ping” toasts stop) and yours fulfills only the second part (when the observer comes back, it immediately sees “bar”).

Now this is the final (hopefully) solution:

class DatesViewModel(private val appl: Application) : AndroidViewModel(appl) {
    val dates: LiveData<String?> = liveData {
        while(true) {
            if (latestValue == null) {
                emit("bar")
                Toast.makeText(appl, "ping", Toast.LENGTH_SHORT).show()
            }
            delay(5_000)
        }
    }
}

I removed the emit("foo") because “foo” was just my loading state. Now I represent the loading state by null. For this I changed LiveData<String> into LiveData<String?>.

Also I’m using latestValue == null to check whether we already emitted data before. The documentation of latestValue actually says:

You can use this value to check what was then latest value emited by your block before it got cancelled.

This is a very wrong way to use a ViewModel.

A ViewModel should never reverence UI elements, including a toast.
There is quite a lot wrong with this code, but the very minimum thing to fix is that the UI should be observing the LiveData object in the fragment, to show the toast. It should not happen in the ViewModel. That’s the entire point of an observable ViewModel.

Your basic issue is that you have a coroutine inside a coroutine.
The outer one has the emit() function, but it looks to me like you are allowing it to finish while the inner coroutine is still running.

Using the view model properly would prevent this from happening.

  • Get rid of the viewModelScope.launch {} coroutine, the liveData {} is already a coroutine.
  • viewModel.dates.observe(owner){ value:String? → Toast.makeText(appl, “ping”, Toast.LENGTH_SHORT).show() }
1 Like

The Toast is there to show me whether the coroutine is still running when there is no observer. It’s just debug code to help me understand how the logic works. It’s not part of the actual application. I should have made that more clear.