My conclusion after using both is that:
onCompletion is useful when the cleanup code needs to know whether the flow succeeded or failed
try/finally is useful when you always want to run the exact same cleanup regardless of whether the flow succeeded or failed
This is because onCompletion receives the failure reason as a parameter, or
null if the flow finished normally without an exception.
myFlow.onCompletion { failure ->
if (failure == null) println("Success") else println("Failure")
}
From the docs:
The key advantage of onCompletion is a nullable Throwable parameter of the lambda that can be used to determine whether the flow collection was completed normally or exceptionally.
https://kotlinlang.org/docs/flow.html#declarative-handling
To achieve the same thing with try/catch is a bit more verbose (and actually needs a catch instead of a finally:
try {
collect()
} catch (t: Throwable) {
println("Failure")
throw t
}
println("Success")
You mentioned try-with-resources. I think it's important to note the difference between try/finally in a flow builder and try/finally in a flow collector. The former is useful for closing resources used by the flow; the latter is more analogous to onCompletion.
The example above shows a try/catch in a flow collector. It could be easily replaced with try/finally if you don't need to know whether it succeeded or failed. On the other hand, using finally for cleanup in a flow builder would look like this:
flow {
val myResource = openResource()
try {
myResource.values.forEach { emit(it) }
} finally {
myResource.cleanup()
}
}
In reality you'd probably do this with use rather than try/finally. As you noted, it isn't something you could do with onCompletion (or a try/catch/finally in the flow collector), because it relies on state that lives inside the flow builder. I think it's one of the great benefits of flows that each stage of the flow can encapsulate its setup and cleanup in this way.
In actual fact, onCompletion sort of combines the flow collector and flow builder examples above. That's because in the case where the flow terminated without an exception, onCompletion can emit new values. The fact that it can't emit values when the flow failed with an exception is an artificial restriction that, when you look at the source code, actually complicates the implementation.
By writing our own flow operator we can overcome that constraint:
fun <T> Flow<T>.finally(block: suspend FlowCollector<T> -> Unit) {
try {
emitAll(this@finally)
} finally {
block(t)
}
}
With the above operator, someFlow.finally { emit(...) } will always emit a new value, even if the flow failed with an exception. I'm not sure whether it violates the prohibition on wrapping emit with try/catch, but it doesn't feel like a particularly good idea.