Other related articles
Basic introduction to Kotlin coroutines: three startup methods and suspension functions of coroutines
Basic introduction to Kotlin coroutines: Job and the life cycle of coroutines
Introduction to the basics of Kotlin coroutines: Context of coroutines (everything is Context)
Kotlin coroutines and Channel (channel)< /span>
简介
Kotlin
Flow
isKotlin
an important component in asynchronous programming, which provides a declarative, composable, coroutine-based asynchronous programming model. Flow's design is inspired by Reactive Streams
, RxJava
, Flux
and other asynchronous programming libraries, but it is not the same as Kotlin
Coroutines are seamlessly integrated and provide a moreKotlin
API.
Flow
It is a concept used for asynchronous data flow, which can be viewed as a series of asynchronous, concurrent streams of values or events. These streams can be changed, filtered, transformed, and combined, merged, flattened, etc. through operator chains. Data in Flow can be asynchronous, non-blocking, or lazy loaded, so it is very suitable for processing asynchronous tasks such as network requests, database queries, sensor data, etc.
Usage example
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
Do you feel familiar when you see this kind of chain call? Yes, just likeRxJava
, Kotlin'sFlow
is also divided into upstream and downstream. emm... You can also understand that Kotlin's Flow
is to replace RxJava
. If you have a certain understanding of RxJava
, it will be easier to learn Flow. .
Application scenarios
- Asynchronous task processing:
Flow
can easily handle asynchronous tasks, such as network requests, database queries, etc. - UI event response:
Flow
can be used to handle UI events, such as button clicks, search operations, etc. - Data flow pipeline:
Flow
can be used as a data processing pipeline, continuously emitting data from a data source for subsequent processing or display. - Data flow conversion:
Flow
You can easily convert, filter, group and other operations on data to implement complex data flow processing logic.
Let’s take a look at some common Flow usage examples to feel the charm of Flow:
Flow provides various operators for transforming, filtering and combining flows, such as map()
, filter()
, transform()
, zip()
, flatMapConcat()
, etc. These operators can be used to chain operations on streams through chained calls.
Flow also has backpressure support and can control the flow through operators such as buffer()
, conflate()
, collectLatest()
etc. sending rate to avoid resource imbalance issues between producers and consumers.
1. Conversion operation
map()
: Converts each element in the Flow to another type.
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()
Functions have the following overloaded forms:
take(n: Int)
: Get the first n elements from the stream.takeWhile(predicate: (T) -> Boolean)
: Get elements that satisfy the condition until the first element that does not satisfy the condition is encountered.takeLast(n: Int)
: Get the last n elements from the end of the stream.
filter()
: Filter elements in Flow based on the given predicate function.
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. Combination operations
zip()
: Combine elements of two Flows together one-to-one.
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()
: Flatten elements in a Flow into multiple Flows and connect them in order.
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。以此类推...
In addition to using flow{}
to create Flow
, you can also use flowOf()
this function
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
The function is a convenient way to quickly create Flow
. It accepts a variable number of arguments and puts them into the Flow as emitters. This way we can specify the elements to emit directly in flowOf
without using the stream builder flow { }
.
In some scenarios, we can even use Flow as a collection, or conversely, use a collection as a Flow.
Flow.toList():
toList()
is a terminal operator within Kotlin Flow
. It is used to collect the elements in Flow
into a list and return it in that list. It collects all elements in Flow
and then returns a list containing all elements when the stream completes.
Here are example uses of 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
requires attention, the toList() operator waits for the entire stream to complete before collecting all elements into the list. Therefore, if the Flow is an infinite flow, it may never complete, or may not complete before memory and computing resources are exhausted.
List.asFlow()
:
asFlow()
is an extension function of the class in the Kotlin
standard library, used to convert < /span> one by one as emitted items. to be sent to . It allows the elements in is converted to List
List
Flow
List
Flow
Here are example uses of 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
The role of asFlow() is to convert other data structures with iterative properties (such as List, Array, etc.) into Flow so that these data can be processed using Flow's operators and functions. This is useful for integrating with existing data structures in streaming data processing.
It's worth noting that a Flow converted using asFlow() follows the order of the iterator when sending elements. That is, the elements emitted by Flow are in the same order as the elements in the original data structure (such as a List).
There are three ways to create Flow known so far:
Flow creation method | Applicable scene | usage |
---|---|---|
flow{} | unknown data set | flow { emit(getLock()) } |
flowOf() | Known specific data | flow(1,2,3) |
asFlow() | data collection | list.asFlow() |
From the above code example, we can see that Flow's API is generally divided into three parts: upstream, intermediate operations, and downstream. The upstream sends data, and the downstream receives and processes data. The most complex one is the intermediate operator. The following is a detailed introduction to the middle of Flow. operator.
intermediate operator
life cycle
Before learning the intermediate operators, first understandFlow
生命周期
- Flow creation: Create a flow by using the
flow { ... }
builder or otherFlow
builders. At this stage, the stream is cold and will not emit any values. - Flow collection: By calling the
collect
function or other flow collection operators (such astoList
,first
,reduce
, etc.) to collect the value of the stream. At this stage, the stream starts emitting values and triggering related operations. - Flow completion: When all values emitted have been consumed, the flow is completed and marked as completed. At this point, the life cycle of the stream ends.
- Flow cancellation: If the code block that collects the flow is canceled (using the coroutine's
cancel
function), or the collector of the flow is destroyed (such asActivity
orFragment
is destroyed), the collection process of the stream will be cancelled.
It should be noted that Flow is based on coroutines, so its life cycle is closely related to the life cycle of coroutines. When a coroutine is canceled, the flow collection associated with the coroutine will also be cancelled. Therefore, when using Flow to encapsulate network requests, if you want to cancel a request, just cancel the corresponding coroutine.
Looking aheadonStart
,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
The function allows you to perform some operations before the Flow starts emitting elements, including adding logs, initialization operations, etc.
onCompletion
The function allows you to perform some operations after the Flow is completed, including resource cleanup, finishing operations, etc.
andonCompletion{}
will call back in the following three situations:
- Normal execution completed
- Abnormal
- got canceled
Exception handling
Thecatch
operator in Flow is used to catch exceptions in the flow. Considering that Flow has upstream and downstream characteristics, the role of the catch operator is strongly related to its position. That is, only upstream exceptions can be caught but downstream exceptions cannot be caught. Pay attention to the position of cache
when using it.
Look at an example
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
It should be noted that the execution order ofcatch
and onCompletion
is related to their location. When an exception occurs, whoever is upstream will execute first.
context switch
Flow
Great for complex asynchronous tasks. In most asynchronous tasks, we need to frequently switch working threads. For time-consuming tasks, we need to execute them in the thread pool, and for UI tasks, we need to execute them on the main thread.
flowOn
We can perfectly solve this problem
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
When is not used by defaultflowOn
, all code in Flow is executed on the main thread scheduler. When using flowOn
to switch the context environment After that, the flowOn
上游
code will be executed in its specified context. Like the cache
operatorflowOn
, its position is also strongly related.
launchIn
Operator used to initiate a collection operation for a stream
launchIn
The syntax of the operator is as follows:
flow.launchIn(scope)
Among them, flow is the flow to be collected, and scope is the coroutine scope used to start flow collection.
The following are two examples to feel the effect oflaunchIn
:
Example 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()
}
Example 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
Source code
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}
The function of the onEach
operator in the above code is to process each element in the stream without changing the elements in the stream. It is similar to forEach or map operations in other programming languages, but onEach does not return a modified stream but continues to return the original stream.
SincelaunchIn
calls collect(), it is also a termination operator. Both of the above methods can switch the context environment in collect
. It seems weird, because wouldn't it be more convenient to usewithContext{}
in the coroutine scope? However, the greater role of launchIn
is to allow other operators such as collect{}
, filter{}
, etc. to run in the specified context.
If the above two examples are not easy to understand, take a look at these two
Example 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
Example 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
It's clear at a glance, right?
Note: Since using withContext directly in Flow can easily cause other problems, withContext is not recommended in Flow. Even if it is used, it should be used with caution.
Termination operator
Termination operators in Flow include the following:
collect
: Collect elements in the stream and perform corresponding operations.toList
,toSet
: Collect streams into lists or sets.reduce
,fold
: Combine the elements of the stream into a single value using the given accumulator function.
It should be noted that no other operators can be clicked after the termination operator, it can only be the last operator of Flow!
Why is Flow called "cold"? What is the difference from Channel?
Flow
The main reason is called "cold" is that it is a lazy data stream. A cold stream means that when no collectors subscribe to the stream, it will not produce any data. The execution of Flow
is driven by the needs of the collector. Only when one or more collectors subscribe to Flow
and call < a i=3> will not start transmitting data until the collection operation occurs. collect
Flow
Flow and Channel features:
-
Flow
is a lazy data flow:Flow
is an asynchronous data flow processing library based on coroutines, and reactive programming is introduced inKotlin
Thought. Compared with other reactive streaming frameworks (such asRxJava
),Flow
is lazy and will only start emitting data when a collector subscribes. This makesFlow
ideal for handling potentially infinite sequences or large amounts of data that need to be processed asynchronously. -
Channel
is a hot channel:Channel
is the mechanism used inKotlin
for communication and collaborative work between coroutines. UnlikeFlow
,Channel
is hot and will continue to emit data even if there is no receiver. It can be used to transfer data between multiple coroutines, perform asynchronous message passing, etc.
the difference:
-
Flow
is a model based on passive subscription, and the emission of data is driven by the needs of the collector. Each collector subscribes independentlyFlow
and can process the data at its own pace. -
Channel
is a model that actively pushes data, and data is sent and received explicitly. The sender can put data intoChannel
, and the receiver actively obtains the data by calling the function ofChannel
.receive()
Applicable scene
-
Flow
is suitable for processing asynchronous data streams, such as network request results, database query results, etc. It provides various operators (such asmap
,filter
,transform
, etc.) to convert and process data streams, and supports backend Backpressure processing to avoid pressure imbalance between producers and consumers. -
Channel
Suitable for communication and collaborative work between multiple coroutines. It allows coroutines to send and receive data asynchronously, and can be used to implement producer-consumer models, event-driven models, etc.
Thanks to: Zhu Tao · Kotlin Programming First Lesson
Since I am a beginner, I don’t have a deep understanding of coroutines. If there are any mistakes in the description, I welcome criticisms and corrections, and I will not hesitate to teach you.