その他の関連記事
Kotlin コルーチンの基本入門: コルーチンの 3 つの起動方法と一時停止機能
Kotlin コルーチンの基本入門: 仕事と生活コルーチンのサイクル
Kotlin コルーチンの基本の紹介: コルーチンのコンテキスト (すべてがコンテキスト)
Kotlin コルーチンとチャネル (チャネル)
简介
Kotlin
Flow
はKotlin
非同期プログラミングの重要なコンポーネントであり、宣言的で構成可能なコルーチンベースの非同期プログラミング モデルを提供します。 Flow の設計は Reactive Streams
、 RxJava
、 Flux
およびその他の非同期プログラミング ライブラリからインスピレーションを得ていますが、 API を提供します。 Kotlin
コルーチンはシームレスに統合され、より多くのKotlin
Flow
これは非同期データ フローに使用される概念であり、値またはイベントの一連の非同期同時ストリームとみなすことができます。これらのストリームは、演算子チェーンを通じて変更、フィルタリング、変換、結合、マージ、フラット化などを行うことができます。フロー内のデータは、非同期、ノンブロッキング、または遅延ロードできるため、ネットワーク リクエスト、データベース クエリ、センサー データなどの非同期タスクの処理に非常に適しています。
使用例
fun main() = runBlocking {
flow {
// 上游,发源地
emit(111) //挂起函数
emit(222)
emit(333)
emit(444)
emit(555)
}.filter {
it > 200 } //过滤
.map {
it * 2 } //转换
.take(2) //从流中获取前 2 个元素
.collect {
println(it)
}
}
//对应输出
444
666
Process finished with exit code 0
この種のチェーン呼び出しを見ると、見覚えがあると思いませんか? はい、RxJava
と同じように、Kotlin のFlow
も上流と下流に分かれています。えっと...Kotlin の Flow
が RxJava
を置き換えるものであることも理解できます。RxJava
をある程度理解していれば、フローを学びやすくなります。
アプリケーションシナリオ
- 非同期タスク処理:
Flow
は、ネットワーク リクエスト、データベース クエリなどの非同期タスクを簡単に処理できます。 - UI イベント レスポンス:
Flow
は、ボタンのクリックや検索操作などの UI イベントを処理するために使用できます。 - データ フロー パイプライン:
Flow
は、後続の処理または表示のためにデータ ソースからデータを継続的に出力するデータ処理パイプラインとして使用できます。 - データ フロー変換:
Flow
データの変換、フィルター、グループ化などの操作を簡単に実行して、複雑なデータ フロー処理ロジックを実装できます。
Flow の魅力を感じるために、一般的な Flow の使用例をいくつか見てみましょう。
フローには、map()
、filter()
、transform()
、< など、フローの変換、フィルタリング、結合のためのさまざまな演算子が用意されています。 /span> など。これらの演算子を使用すると、連鎖呼び出しを通じてストリームに対する操作を連鎖させることができます。 zip()
、flatMapConcat()
フローにはバックプレッシャーもサポートされており、buffer()
、conflate()
、collectLatest()
などの演算子を使用してフローを制御できます。生産者と消費者間のリソースの不均衡の問題を回避するための送信レート。
1. 変換操作
map()
: フロー内の各要素を別の型に変換します。
fun createFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}
fun main() = runBlocking {
createFlow()
.map {
it * it } // 将元素平方
.collect {
value ->
println(value) // 打印平方后的值
}
}
//对应输出
1
4
9
16
25
Process finished with exit code 0
-
take()
関数には次のようなオーバーロードされた形式があります。
take(n: Int)
: ストリームから最初の n 要素を取得します。takeWhile(predicate: (T) -> Boolean)
: 条件を満たさない最初の要素が見つかるまで、条件を満たす要素を取得します。takeLast(n: Int)
: ストリームの最後から最後の n 要素を取得します。
filter()
: 指定された述語関数に基づいてフロー内の要素をフィルターします。
fun createFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}
fun main() = runBlocking {
createFlow()
.filter {
it % 2 == 0 } // 过滤偶数
.collect {
value ->
println(value) // 打印偶数
}
}
//对应输出
2
4
Process finished with exit code 0
2. 組み合わせ操作
zip()
: 2 つのフローの要素を 1 対 1 で結合します。
fun createFlowA(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}
fun createFlowB(): Flow<String> = flow {
for (i in 5 downTo 1) {
delay(1000)
emit("Item $i")
}
}
fun main() = runBlocking {
createFlowA()
.zip(createFlowB()) {
a, b -> "$a - $b" } // 组合 FlowA 和 FlowB 的元素
.collect {
value ->
println(value) // 打印组合后的元素
}
}
//对应输出
1 - Item 5
2 - Item 4
3 - Item 3
4 - Item 2
5 - Item 1
Process finished with exit code 0
flatMapConcat()
: フロー内の要素を複数のフローにフラット化し、それらを順番に接続します。
fun createFlowOfList(): Flow<List<Int>> = flow {
for (i in 1..3) {
delay(1000)
emit(List(i) {
it * it }) // 发出包含整数平方的列表
}
}
fun main() = runBlocking {
createFlowOfList()
.flatMapConcat {
flowOfList -> flowOfList.asFlow() } // 扁平化列表中的元素
.collect {
value ->
println(value) // 打印平方后的值
}
}
//对应输出
0
0
1
0
1
4
Process finished with exit code 0
解释下为什么是这样的打印结果,因为上面发送了三个Flow<List<Int>>,第一个List元素个
数为1所以打印 索引的平方即只有一个元素 下表索引就是0,输出打印0的平方还是0,第二个List
元素个数为2,返回索引下标0,1,扁平化List后打印 0,1。以此类推...
flow{}
を使用して Flow
を作成するだけでなく、flowOf()
この関数 を使用することもできます。
fun main() = runBlocking {
flowOf(1, 2, 3, 4, 5)
.collect {
value ->
println(value) // 打印 Flow 中的元素
}
}
//对应输出
1
2
3
4
5
Process finished with exit code 0
flowOf
関数は、Flow
をすばやく作成するのに便利な方法です。可変数の引数を受け入れ、それらをエミッターとしてフローに追加します。この方法により、ストリーム ビルダー を使用せずに、 flowOf
で直接出力する要素を指定できます。 flow { }
シナリオによっては、フローをコレクションとして使用したり、逆にコレクションをフローとして使用したりすることもできます。
Flow.toList():
toList()
は、Kotlin Flow
内の端末オペレータです。 Flow
内の要素をリストに収集し、そのリストで返すために使用されます。 Flow
内のすべての要素を収集し、ストリームが完了すると、すべての要素を含むリストを返します。
toList()
の使用例は次のとおりです。
fun createFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}
fun main() = runBlocking {
val list: List<Int> = createFlow()
.toList() // 将 Flow 中的元素收集到列表中
println(list) // 打印列表
}
//对应输出
[1, 2, 3, 4, 5]
Process finished with exit code 0
注意が必要です の場合、toList() オペレーターは、ストリーム全体が完了するのを待ってから、すべての要素をリストに収集します。したがって、フローが無限フローである場合、フローは決して完了しないか、メモリとコンピューティング リソースが使い果たされる前に完了しない可能性があります。
List.asFlow()
:
asFlow()
は、 Kotlin
標準ライブラリの List
クラスの拡張関数であり、使用されます。変換する List
は Flow
に変換されます。これにより、 List
内の要素を 1 つずつ Flow
に放出アイテムとして送信できるようになります。
asFlow()
の使用例は次のとおりです。
fun main() = runBlocking {
val list = listOf(1, 2, 3, 4, 5)
list
.asFlow() // 将 List 转换为 Flow
.collect {
value ->
println(value) // 打印 Flow 中的元素
}
}
//对应输出
1
2
3
4
5
Process finished with exit code 0
asFlow() の役割は、反復プロパティを持つ他のデータ構造 (リスト、配列など) をフローに変換し、これらのデータをフローの演算子と関数を使用して処理できるようにすることです。これは、ストリーミング データ処理で既存のデータ構造と統合する場合に役立ちます。
asFlow() を使用して変換されたフローは、要素を送信するときにイテレーターの順序に従うことに注意してください。つまり、フローによって発行される要素は、元のデータ構造 (リストなど) の要素と同じ順序になります。
これまでに知られているフローを作成するには 3 つの方法があります。
フロー作成方法 | 該当シーン | 使用法 |
---|---|---|
流れ{} | 未知のデータセット | フロー { エミット(getLock()) } |
flowOf() | 既知の特定のデータ | フロー(1,2,3) |
asFlow() | データ収集 | list.asFlow() |
上記のコード例から、Flow の API は通常、上流、中間操作、下流の 3 つの部分に分かれていることがわかります。上流はデータを送信し、下流はデータを受信して処理します。最も複雑なものは中間演算子です。以下は、Flow. 演算子の途中について詳しく説明します。
中間演算子
ライフサイクル
中間演算子を学ぶ前に、まず理解してくださいFlow
生命周期
- フローの作成:
flow { ... }
ビルダーまたは他のFlow
ビルダーを使用してフローを作成します。この段階では、ストリームはコールドであり、値を出力しません。 - フロー収集:
collect
関数または他のフロー収集演算子 (toList
、first
、< など) を呼び出します。 /span>reduce
など) を使用してストリームの値を収集します。この段階で、ストリームは値の発行と関連操作のトリガーを開始します。 - フローの完了: 発行されたすべての値が消費されると、フローは完了し、完了としてマークされます。この時点で、ストリームのライフサイクルは終了します。
- フローのキャンセル: フローを収集するコード ブロックがキャンセルされた場合 (コルーチンの
cancel
関数を使用)、またはフローのコレクターが破棄された場合 ( < a i=2> または が破棄された場合)、ストリームの収集処理はキャンセルされます。Activity
Fragment
フローはコルーチンに基づいているため、そのライフサイクルはコルーチンのライフサイクルと密接に関連していることに注意してください。コルーチンがキャンセルされると、コルーチンに関連付けられたフロー収集もキャンセルされるため、フローを使用してネットワーク リクエストをカプセル化する場合、リクエストをキャンセルしたい場合は、対応するコルーチンをキャンセルするだけです。
先来看下onStart
、onCompletion
fun main() = runBlocking {
flow {
emit(1)
emit(2)
emit(3)
}.onStart {
println("Flow started emitting values")
}.onCompletion {
println("Flow completed")
}.collect {
value ->
println("Received value: $value")
}
}
//对应输出
Flow started emitting values
Received value: 1
Received value: 2
Received value: 3
Flow completed
Process finished with exit code 0
onStart
関数を使用すると、フローが要素の出力を開始する前に、ログの追加や初期化操作などのいくつかの操作を実行できます。
onCompletion
この関数を使用すると、フローの完了後にリソースのクリーンアップや終了操作などの一部の操作を実行できます。
およびonCompletion{}
は、次の 3 つの状況でコールバックします。
- 通常の実行が完了しました
- 異常な
- キャンセルされました
例外処理
フローの catch
演算子は、フロー内の例外をキャッチするために使用されます。フローには上流と下流の特性があることを考慮すると、キャッチ 演算子の役割はその位置と強く関連しています。つまり、上流の例外のみをキャッチでき、下流の例外はキャッチできないため、使用する場合は cache
の位置に注意してください。
例を見てみましょう
fun main() = runBlocking {
flow {
emit(1)
emit(2)
throw NullPointerException("Null error")
emit(3)
}.onStart {
println("Flow started emitting values")
}.catch {
println("Flow catch")
emit(-1)
}.onCompletion {
println("Flow completed")
}.collect {
value ->
println("Received value: $value")
}
}
//对应输出
Flow started emitting values
Received value: 1
Received value: 2
Flow catch
Received value: -1
Flow completed
Process finished with exit code 0
catch
と onCompletion
の実行順序は、その場所に関係していることに注意してください。例外が発生すると、上流にある人が最初に実行されます。 。
コンテキストスイッチ
Flow
複雑な非同期タスクに最適です。ほとんどの非同期タスクでは、作業スレッドを頻繁に切り替える必要があります。時間のかかるタスクの場合はスレッド プールで実行する必要があり、UI タスクの場合はメイン スレッドで実行する必要があります。
flowOn
私たちはこの問題を完全に解決できます
fun main() = runBlocking {
flow {
emit(1)
println("emit 1 in thread ${
Thread.currentThread().name}")
emit(2)
println("emit 2 in thread ${
Thread.currentThread().name}")
emit(3)
println("emit 3 in thread ${
Thread.currentThread().name}")
}.flowOn(Dispatchers.IO)
.collect {
println("Collected $it in thread ${
Thread.currentThread().name}")
}
}
//对应输出
emit 1 in thread DefaultDispatcher-worker-2
emit 2 in thread DefaultDispatcher-worker-2
emit 3 in thread DefaultDispatcher-worker-2
Collected 1 in thread main
Collected 2 in thread main
Collected 3 in thread main
Process finished with exit code 0
がデフォルトで使用されていない場合flowOn
、フロー内のすべてのコードはメインスレッド スケジューラで実行されます。flowOn
を使用してコンテキスト環境を切り替える場合つまり、flowOn
上游
コードは指定されたコンテキストで実行されます。cache
演算子flowOn
と同様に、その位置も強く関連しています。
launchIn
ストリームの収集操作を開始するために使用される演算子
launchIn
演算子の構文は次のとおりです。
flow.launchIn(scope)
このうち、flow は収集対象のフロー、scope はフロー収集を開始するコルーチンのスコープです。
次は、launchIn
の効果を実感できる 2 つの例です。
例 1:
fun main() = runBlocking {
val flow = flow {
emit(1)
emit(2)
emit(3)
}
val job = launch(Dispatchers.Default) {
flow.collect {
value ->
println("Collecting $value in thread ${
Thread.currentThread().name}")
}
}
delay(1000)
job.cancel()
}
例 2:
fun main() = runBlocking(Dispatchers.Default) {
val flow = flow {
emit(1)
emit(2)
emit(3)
}
flow.flowOn(Dispatchers.IO)
.onEach {
println("Flow onEach $it in thread ${
Thread.currentThread().name}")
}.launchIn(this)
delay(1000)
}
launchIn
ソースコード
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}
上記のコードの onEach
演算子の機能は、ストリーム内の要素を変更せずにストリーム内の各要素を処理することです。これは他のプログラミング言語の forEach またはマップ操作に似ていますが、onEach は変更されたストリームを返さず、元のストリームを返し続けます。
launchIn
はcollect() を呼び出すため、終了演算子でもあります。上記のメソッドは両方とも、collect
でコンテキスト環境を切り替えることができます。奇妙なことに、 コルーチン スコープでwithContext{}
を使用した方が便利ではないでしょうか?ただし、 launchIn
のより大きな役割は、 collect{}
、 filter{}
などの他の演算子を指定されたコンテキストで実行できるようにすることです。 。
上記 2 つの例がわかりにくい場合は、次の 2 つの例を見てください。
例 1:
fun main() = runBlocking {
val scope = CoroutineScope(Dispatchers.IO)
val flow = flow {
emit(1)
println("Flow emit 1 in thread ${
Thread.currentThread().name}")
emit(2)
println("Flow emit 2 in thread ${
Thread.currentThread().name}")
emit(3)
println("Flow emit 3 in thread ${
Thread.currentThread().name}")
}
flow.filter {
println("Flow filter in thread ${
Thread.currentThread().name}")
it > 1
}.onEach {
println("Flow onEach $it in thread ${
Thread.currentThread().name}")
}.collect()
delay(1000)
}
//对应输出
Flow filter in thread main
Flow emit 1 in thread main
Flow filter in thread main
Flow onEach 2 in thread main
Flow emit 2 in thread main
Flow filter in thread main
Flow onEach 3 in thread main
Flow emit 3 in thread main
Process finished with exit code 0
例 2:
//只是把上面collect 换成了launchIn(scope)
flow.filter {
println("Flow filter in thread ${
Thread.currentThread().name}")
it > 1
}.onEach {
println("Flow onEach $it in thread ${
Thread.currentThread().name}")
}.launchIn(scope)
//对应输出
Flow filter in thread DefaultDispatcher-worker-1
Flow emit 1 in thread DefaultDispatcher-worker-1
Flow filter in thread DefaultDispatcher-worker-1
Flow onEach 2 in thread DefaultDispatcher-worker-1
Flow emit 2 in thread DefaultDispatcher-worker-1
Flow filter in thread DefaultDispatcher-worker-1
Flow onEach 3 in thread DefaultDispatcher-worker-1
Flow emit 3 in thread DefaultDispatcher-worker-1
Process finished with exit code 0
一目瞭然ですよね?
注: withContext をフロー内で直接使用すると別の問題が発生しやすいため、フロー内での withContext の使用は推奨されません。
終端事業者
Flow の終了演算子には次のものがあります。
collect
: ストリーム内の要素を収集し、対応する操作を実行します。toList
、toSet
: ストリームをリストまたはセットに収集します。reduce
、fold
: 指定されたアキュムレータ関数を使用して、ストリームの要素を単一の値に結合します。
終了オペレータの後には他のオペレータをクリックすることはできません。終了オペレータは Flow! の最後のオペレータのみであることに注意してください。
なぜフローは「コールド」と呼ばれるのでしょうか?チャンネルとの違いは何ですか?
Flow
が「コールド」と呼ばれる主な理由は、それが遅延データ ストリームであるためです。コールド ストリームとは、ストリームをサブスクライブするコレクターがいない場合、データが生成されないことを意味します。 Flow
の実行は、コレクターのニーズによって決まります。1 つ以上のコレクターが Flow
をサブスクライブし、 < a i=3> を呼び出した場合にのみ、収集操作が発生するまでデータの送信は開始されません。 collect
Flow
フローとチャネルの機能:
-
Flow
は遅延データ フローです。Flow
はコルーチンに基づく非同期データ フロー処理ライブラリであり、リアクティブ プログラミングはKotlin
の思想で導入されています。他のリアクティブ ストリーミング フレームワーク (RxJava
など) と比較すると、Flow
は遅延しており、コレクターがサブスクライブした場合にのみデータの送信を開始します。このため、Flow
は、潜在的に無限のシーケンスや、非同期で処理する必要がある大量のデータを処理するのに最適です。 -
Channel
はホット チャネルです。Channel
は、コルーチン間の通信と共同作業のためにKotlin
で使用されるメカニズムです。Flow
とは異なり、Channel
は高温であり、受信機がない場合でもデータを送信し続けます。複数のコルーチン間でデータを転送したり、非同期メッセージ パッシングを実行したりするために使用できます。
違い:
-
Flow
は受動的なサブスクリプションに基づくモデルであり、データの送信はコレクターのニーズによって決まります。各コレクターは個別にサブスクライブしFlow
、独自のペースでデータを処理できます。 -
Channel
はアクティブにデータをプッシュするモデルであり、データは明示的に送受信されます。送信者はChannel
にデータを入れることができ、受信者はChannel
のreceive()
関数を呼び出して能動的にデータを取得します。
該当シーン
-
Flow
は、ネットワーク リクエストの結果、データベース クエリの結果などの非同期データ ストリームの処理に適しています。データ ストリームを変換および処理するためのさまざまな演算子 (map
、filter
、transform
など) を提供し、バックエンドをサポートします。生産者と消費者の間の圧力の不均衡を避けるための背圧処理。 -
Channel
複数のコルーチン間の通信や共同作業に適しています。これにより、コルーチンがデータを非同期に送受信できるようになり、プロデューサー/コンシューマー モデル、イベント駆動型モデルなどの実装に使用できます。
感謝: Zhu Tao · Kotlin プログラミング ファースト レッスン
初心者のためコルーチンについては深く理解していませんが、記述に間違いがあればご指摘・修正を歓迎しますので、遠慮なくご指導させていただきます。