https://kotlinlang.org logo
#flow
Title
# flow
m

Marek Kubiczek

08/23/2021, 3:46 PM
Considering following code snippet
Copy code
viewLifecycleOwner.lifecycleScope.launch {
  val result = someFlow.first()
  doSomethingInUI(result)
}
where
doSomethingInUI
is not a
suspend
function. What will happen if scope is destroyed/cancelled while there is ongoing operation
someFlow.first()
(eg. database read). Is
first()
cancellable? Will it exit the launch block gracefully or do I need to always call
ensureActive()
before a call to non suspended function in such case? The same stands for
Copy code
someFlow.onEach {
  doSomnethinginUI(it)
}.launchIn(viewLifecycleOwner.lifecycleScope)
Is there any guarantee
onEach
won’t be called after scope is destroyed/cancelled?
e

ephemient

08/23/2021, 7:04 PM
yes,
.first()
is cancelable. you're not catching
CancellationException
so it will leave scope naturally.
.launchIn()
is equivalent to
launch { .collect() }
, so it'll be sequenced with everything else on Main as long as you haven't switched to another dispatcher with
withContext()
m

Marek Kubiczek

08/24/2021, 1:21 PM
There is something I don’t understand there. I have crash on production where aprarently I land in doSomethingInUi despite the fact view is already destroyed. I can reproduce it with the following code snippet
Copy code
someFlow.onEach {
  nonCancellableDelay()
}.onEach {
  doSomnethinginUI(it)
}.launchIn(viewLifecycleOwner.lifecycleScope)
where the delay is non cancellable and defined as
Copy code
suspend fun nonCancellableDelay() = suspendCoroutine<Unit> {
        Handler(Looper.getMainLooper()).postDelayed(Runnable {
            it.resume(Unit)
        }, 2000)
    }
I would expect the second
onEach
shouldn’t be called in case scope has been destroyed meanwhile. But it’s not the case. Making me think that I can only be safe by calling ensureActive first which is counterintuitive.
e

ephemient

08/24/2021, 4:04 PM
well as you've worn it that ignores cancellation…
suspendCancellableCoroutine would check for cancellation before resuming, but given the naming that seems intentional… why?
m

Marek Kubiczek

08/24/2021, 4:08 PM
This is just some craft made code that I wrote to try to simulate long time needed to process the db query. So I made it noncancellable on purpose, just to ensure it is not cancelled while delayed but I was expected it to be cancelled before next emission from one onEach to the other one.
e

ephemient

08/24/2021, 4:10 PM
onEach all run one after each other and there isn't an intervening suspension point that would throw if the scope gets cancelled
another way to put it, what you wrote is equivalent to
Copy code
launch(lifecycleScope) {
    flow {
        someFlow.collect {
            nonCancellableDelay()
            doSomnethinginUI(it)
            emit(it)
        }
    }.collect()
}
if you take out all the flow stuff, you'll find that
Copy code
launch(lifecycleScope) {
    nonCancellableDelay()
    doSomethingInUi(it)
}
has the same behavior: if there's an actual suspension it'll throw CancellationException, but otherwise it just keeps running
normally this wouldn't arise, because either you've changed contexts away from Main and will check cancellation before resuming back on Main, or you're running blocking code on Main so the UI can't be destroyed asynchronously…
m

Marek Kubiczek

08/26/2021, 6:08 AM
We still believe there is a bug. One of my colleagues investigated it more and reported a bug https://youtrack.jetbrains.com/issue/KT-48401
5 Views