Kotlin coroutines and Flow

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>


简介
KotlinFlow 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.

FlowIt 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:
  1. take(n: Int): Get the first n elements from the stream.
  2. takeWhile(predicate: (T) -> Boolean): Get elements that satisfy the condition until the first element that does not satisfy the condition is encountered.
  3. 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,返回索引下标01,扁平化List后打印 01。以此类推...

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

flowOfThe 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 ListListFlowListFlow

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生命周期

  1. Flow creation: Create a flow by using the flow { ... } builder or other Flow builders. At this stage, the stream is cold and will not emit any values.
  2. Flow collection: By calling the collect function or other flow collection operators (such as toList, first, reduce, etc.) to collect the value of the stream. At this stage, the stream starts emitting values ​​and triggering related operations.
  3. 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.
  4. 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 as Activity or Fragment 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

onStartThe 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:

  1. Normal execution completed
  2. Abnormal
  3. 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

FlowGreat 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.

flowOnWe 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 cacheoperatorflowOn, its position is also strongly related.

launchIn
Operator used to initiate a collection operation for a stream

launchInThe 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)
}

launchInSource 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?

FlowThe 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. collectFlow

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 in Kotlin Thought. Compared with other reactive streaming frameworks (such as RxJava), Flow is lazy and will only start emitting data when a collector subscribes. This makes Flow 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 in Kotlin for communication and collaborative work between coroutines. Unlike Flow, 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 independently Flow 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 into Channel, and the receiver actively obtains the data by calling the function of Channel. 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 as map, filter, transform, etc.) to convert and process data streams, and supports backend Backpressure processing to avoid pressure imbalance between producers and consumers.

  • ChannelSuitable 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.

Guess you like

Origin blog.csdn.net/WriteBug001/article/details/132031020