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.
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.
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()
}
}
}
}
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”).
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.
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.
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.