Chain kotlin flows depends on Result state

4.5k views Asked by At

I'm looking for the most "clean" way to implement the following logic:

  • I have N methods, everyone returns Flow<Result<SOME_TYPE>> (type are different)
  • I want to chain these methods, so if 1 returns Result.Success, then call 2nd and so on.

The most obvious way to do it is:

methodA().map { methodAResult ->
  when (methodAResult) {
    is Result.Success -> {
      methodB(methodAResult).map { methodBResult ->
        when (methodBResult) {
          is Result.Success -> {
            methodC(methodAResult).map { methodCResult ->
              when (methodCResult) {
                is Result.Success -> TODO()
                is Result.Failure -> TODO()
              }
            }
          }
          is Result.Failure -> TODO()
        }
      }
     }
     is Result.Failure -> TODO()
   }
 }

But it looks like a well-known "callback hell". Do u have any ideas how to avoid it?

5

There are 5 answers

4
Михаил Нафталь On BEST ANSWER

I believe this could be flattened with transform operator:

methodA().transform { methodAResult ->
    when (methodAResult) {
        is Success -> methodB(methodAResult).collect { emit(it) }
        is Failure -> TODO()
    }
}.transform { methodBResult ->
    when (methodBResult) {
        is Success -> methodC(methodBResult).collect { emit(it) }
        is Failure -> TODO()
    }
}.transform { methodCResult ->
    when (methodCResult) {
        is Success -> TODO()
        is Failure -> TODO()
    }
}
5
Sir Codesalot On

I believe that in this use case you should probably use suspend functions and compose them using await(). Errors should be passed through exceptions as described here.

0
Michael Krussel On

A slight modification to the solution provided by Михаил Нафталь

    methodA()
        .flatMapMerge {
            when (it) {
                is Result.Success -> methodB(it)
                is Result.Failure -> emptyFlow()
            }
        }.flatMapMerge {
            when (it) {
                is Result.Success -> methodC(it)
                is Result.Failure -> emptyFlow()
            }
        }.collect {
            when (it) {
                is Result.Success -> TODO()
                is Result.Failure -> TODO()
            }
        }

Merging the output of one flow to another flow is the goal of flatMap so using flatMap seems a little cleaner.

If this Result class has a map, fold, or getOrNull type method this could be cleaned up a bit more and the when blocks could be removed.

Also if you need to propagate the failure to collect then you could replace the calls to emptyFlow with a flow that just outputs the failure that you want.

0
suside On

Currently if you are fine with external libs you can use result computation block from Arrow like so:

import arrow.core.raise.result
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.result.shouldBeFailure
import io.kotest.matchers.result.shouldBeSuccess
import io.kotest.matchers.shouldBe
// ...
fun methodA(): Result<String> = Result.success("1")
fun methodB(x: Int): Result<Int> = Result.success(x + 2)
fun methodC(x: String): Result<String> = Result.failure(Exception("Fail $x"))

result {
    val myint = methodA().bind().toInt()
    val mystring = methodB(myint).bind().toString()
    methodC(mystring).bind()
}.shouldBeFailure {
    it.message shouldBe "Fail 3"
}

result {
    val myint = methodA().bind().toInt()
    methodB(myint).bind().toString()
}.shouldBeSuccess {
    it shouldBe "3"
}
0
DamienL On

Badly a flatMap method still doesn't exist.

But you can use mapCatching :

methodA
    .mapCatching { a -> methodB(a).getOrThrow() }
    .mapCatching { b -> methodC(b).getOrThrow() }

Or make your own flatMap extension function :

fun <T, R> Result<T>.flatMap(block: (T) -> (Result<R>)): Result<R> {
    return this.mapCatching {
        block(it).getOrThrow()
    }
}

methodA
    .flatMap { a -> methodB(a) }
    .flatMap { b -> methodC(b) }