Recently I've updated my project to Kotlin 1.5 and noticed some weird and unexpected behaviour when combining Flow with Result. Consider the following example:
// Kotlin 1.5.0
// Coroutines (kotlinx-coroutines-core) 1.5.0
class Foo
interface FooListener {
fun onSuccess(foo: Foo)
fun onFailure(error: Throwable?)
}
suspend fun collectData(listener: FooListener) {
flow { emit(Result.success(Foo())) }
.collect { result -> // for reference
when {
result.isSuccess -> listener.onSuccess(result.getOrThrow())
result.isFailure -> listener.onFailure(result.exceptionOrNull())
}
}
}
fun main() {
runBlocking {
collectData(object : FooListener {
override fun onSuccess(foo: Foo) {
println(foo)
}
override fun onFailure(error: Throwable?) {
println(error)
}
})
}
}
The above compiles but fails in runtime with:
Exception in thread "main" java.lang.ClassCastException: kotlin.Result cannot be cast to Foo
After some debugging I noticed that the collected value (result) appears to be of type Result<Result<Foo>> instead of the expected Result<Foo>:
suspend fun collectData(listener: FooListener) {
flow { emit(Result.success(Foo())) }
.collect { result ->
println(result) // Output: Success(Success(Foo@...))
when {
result.isSuccess -> listener.onSuccess(result.getOrThrow())
result.isFailure -> listener.onFailure(result.exceptionOrNull())
}
}
}
The problem disappears when I give up an idea of using externally provided listeners or callback functions in collectData and, for example, use something defined globally:
class Foo
interface FooListener {
fun onSuccess(foo: Foo)
fun onFailure(error: Throwable?)
}
val listener = object : FooListener {
override fun onSuccess(foo: Foo) {
println(foo)
}
override fun onFailure(error: Throwable?) {
println(error)
}
}
suspend fun collectData() {
flow { emit(Result.success(Foo())) }
.collect { result ->
println(result) // Output: Success(Foo@...)
when {
result.isSuccess -> listener.onSuccess(result.getOrThrow())
result.isFailure -> listener.onFailure(result.exceptionOrNull())
}
}
}
fun main() {
runBlocking {
collectData()
}
}
The above runs without any errors.
At this point it seems to me like a bug in the language, but I've been onto this for so long I'm not 100% sure and I could use a second opinion before bringing it up on Kotlin YouTrack.
I've seen Why does Kotlin crash passing generic Result parameter? and JVM / IR: ClassCastException with Result object when it is used by a generic method in a suspend call but the bug not only looks the other way around, but it is supposed to be fixed in the newest version as well.