web-dev-qa-db-ja.com

最初の値を待たずにリスト内の複数のKotlinフローを組み合わせる

List<Flow<T>>を持っています。Flow<List<T>>を生成したいと思います。これは combine が行うこととほぼ同じです。ただし、結合がすべてのFlowが初期値を出力するのを待機しますが、これは私が望むことではありません。たとえば、次のコードを見てください。

val a = flow {
  repeat(3) {
    emit("a$it")
    delay(100)
  }
}
val b = flow {
  repeat(3) {
    delay(150)
    emit("b$it")
  }
}
val c = flow {
  delay(400)
  emit("c")
}
val flows = listOf(a, b, c)
runBlocking {
  combine(flows) {
    it.toList()
  }.collect { println(it) }
}

combine(したがって現状のまま)では、これは出力です。

[a2, b1, c]
[a2, b2, c]

それに対して、すべての中間ステップにも興味があります。これは、これらの3つのフローに必要なものです。

[]
[a0]
[a1]
[a1, b0]
[a2, b0]
[a2, b1]
[a2, b1, c]
[a2, b2, c]

現在、私には2つの回避策がありますが、どれも素晴らしいものではありません...最初の回避策は見苦しく、null許容型では機能しません。

val flows = listOf(a, b, c).map {
  flow {
    emit(null)
    it.collect { emit(it) }
  }
}
runBlocking {
  combine(flows) {
    it.filterNotNull()
  }.collect { println(it) }
}

すべてのフローに最初の無関係な値を強制的に出力させることにより、combineトランスフォーマーは実際に呼び出され、実際の値ではないことがわかっているnull値を削除できます。それを繰り返し、より読みやすく、重いです:

sealed class FlowValueHolder {
  object None : FlowValueHolder()
  data class Some<T>(val value: T) : FlowValueHolder()
}
val flows = listOf(a, b, c).map {
  flow {
    emit(FlowValueHolder.None)
    it.collect { emit(FlowValueHolder.Some(it)) }
  }
}
runBlocking {
  combine(flows) {
    it.filterIsInstance(FlowValueHolder.Some::class.Java)
      .map { it.value }
  }.collect { println(it) }
}

これは問題なく機能しますが、それでも私はやりすぎだと感じています。コルーチンライブラリに欠けているメソッドはありますか?

4

まだ中間ラッパータイプへのマッピングを避けたいのですが、誰かがコメントで述べたように、動作は少し間違っています(引数がまだ何も出力していない場合、最初は空のリストが出力されます)が、これはソリューションよりも少し優れています私が質問を書いたとき(まだ本当に似ていて)、null許容型で動作することを念頭に置いていました。

inline fun <reified T> instantCombine(
  flows: Iterable<Flow<T>>
): Flow<List<T>> = combine(flows.map { flow ->
  flow.map {
    @Suppress("USELESS_CAST") // Required for onStart(null)
    Holder(it) as Holder<T>?
  }
    .onStart { emit(null) }
}) {
  it.filterNotNull()
    .map { holder -> holder.value }
}

そして、これはこの実装で合格するテストスイートです:

class InstantCombineTest {
  @Test
  fun `when no flows are merged, nothing is emitted`() = runBlockingTest {
    assertThat(instantCombine(emptyList<Flow<String>>()).toList())
      .isEmpty()
  }

  @Test
  fun `intermediate steps are emitted`() = runBlockingTest {
    val a = flow {
      delay(20)
      repeat(3) {
        emit("a$it")
        delay(100)
      }
    }
    val b = flow {
      repeat(3) {
        delay(150)
        emit("b$it")
      }
    }
    val c = flow {
      delay(400)
      emit("c")
    }

    assertThat(instantCombine(a, b, c).toList())
      .containsExactly(
        emptyList<String>(),
        listOf("a0"),
        listOf("a1"),
        listOf("a1", "b0"),
        listOf("a2", "b0"),
        listOf("a2", "b1"),
        listOf("a2", "b1", "c"),
        listOf("a2", "b2", "c")
      )
      .inOrder()
  }

  @Test
  fun `a single flow is mirrored`() = runBlockingTest {
    val a = flow {
      delay(20)
      repeat(3) {
        emit("a$it")
        delay(100)
      }
    }

    assertThat(instantCombine(a).toList())
      .containsExactly(
        emptyList<String>(),
        listOf("a0"),
        listOf("a1"),
        listOf("a2")
      )
      .inOrder()
  }

  @Test
  fun `null values are kept`() = runBlockingTest {
    val a = flow {
      emit("a")
      emit(null)
      emit("b")
    }

    assertThat(instantCombine(a).toList())
      .containsExactly(
        emptyList<String?>(),
        listOf("a"),
        listOf(null),
        listOf("b")
      )
      .inOrder()
  }
}
0