Kotlin: It takes less than two minutes to understand Flow

This article is based on the typical flow implementation of flow. The sample code is as follows. It is not clear that you need to understand the basic call flow of Flow.

Call example

  • Test code
@Test
fun testColdFeature() = runBlocking{
    flow<Int>{
        println("flow#emit lamda: emit 1")
        emit(1)
        println("flow#emit lamda: emit 2")
        emit(2)
    }.collect{
        println("Collector#collect: ${it}")
    }
}
  • Output result
flow#emit lamda: emit 1
SafeCollector#emit#1
Collector#collect: 1
flow#emit lamda: emit 2
SafeCollector#emit#2
Collector#collect: 2

When the above code calls collect, every emit action will trigger a print operation of collect lamda. This article will analyze the entire call process

Flow core process realization

Core process

interface Flow<out T> {

    suspend fun collect(collector: FlowCollector<T>)
}

All the core processes of Flow are actually carried out around the Flow interface. The concept of Flow can be described in terms of production and consumption. Production is responsible for producing data, such as emit, and consumption is responsible for consuming data. Calling collect in the above interface actually triggers the start of data consumption, more specifically When the data is finally consumed, it will be sent to FlowCollector for processing

interface FlowCollector<in T> {

    suspend fun emit(value: T)
}

FlowCollector was originally understood as data collection, but there is an emit method. Isn't it sending data? Please look down:

suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
    collect(object : FlowCollector<T> {
        override suspend fun emit(value: T) = action(value)
    })

This is the lamda definition of our usual data processing. First call the collect method and use the Flow implementation class to start consuming data. This time is to trigger the production process to mediate the emit method to bring the value over, and the above "emit(value: T) = action(value) )" The value brought over is directly directed to the lamda block for consumption.
Therefore, in the entire production and consumption model, it is like your sister (Flow) is hungry and roars: "I am running out of food, go to make money?" (collect), as a warm man, you (FlowCollector) immediately dispatched full production. After selling a biscuits and handing money to the girl (emit), the girl can directly place an order to buy a mask action (value). So the sequence looks like:

flow#collect  -> produce#emit -> FlowCollector#emit -> collect#cosume

On this basis, we can further understand that both Flow and FlowCollector can be further socketed to enrich and expand more functions.

Specific production mode realization

In order to understand the implementation process of the code of the production mode, here is a separation of the core original code, the following is a simple sorting of the order of the call stack, first of all, the above picture, the call relationship from outside to inside:

1. Trigger consumption data

flow<Int>{
    println("flow#emit lamda: emit 1")
    emit(1)
    println("flow#emit lamda: emit 2")
    emit(2)
}.collect{
    println("Collector#collect: ${it}")
}

The extension method for consumption data is as follows, essentially calling the collect method in Flow

suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
        collect(object : FlowCollector<T> {
            override suspend fun emit(value: T) = action(value)
        })

Action is the lamda function of the final consumption data. The emit of the upper-level FlowCollector is triggered, so who will call the emit

2. Package data realization

class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : Flow<T> {
    override suspend fun collect(collector: FlowCollector<T>) {
        SafeCollector(collector).block()
    }
}

In step 1, the typical implementation class of Flow is SafeFlow, which is actually the loading of SafeCollector. After the previous call, it transfers to the lamda method of SafeCollector.

3. Data production

flow<Int>{
    println("flow#emit lamda: emit 1")
    emit(1)
    println("flow#emit lamda: emit 2")
    emit(2)
}
class SafeCollector<T>(private val collector: FlowCollector<T>) : FlowCollector<T> {
    override suspend fun emit(value: T) {
        println("SafeCollector#emit#${value}")
        collector.emit(value)
    }

}

The production of data is through the emit method, which is called the emit in SafeCollector. In this emit method, the "collector.emit(value)" in the construction parameter is finally called, because it has been called until the lamda method in collect in step 1. Because this lamda is also packaged as a FlowCollector.

Responsibility summary

From the above process, the relationship between Flow and Collector is also clear. Flow has only one collect method, which is responsible for issuing orders and transmitting Collector, while Collector is responsible for collecting and transmitting data corresponding to the collection function.
At the same time, Flow and Collector are both functional attributes that can be continuously connected to samples through the decoration mode like constantly wearing clothes. This will be explained later.

Easily understand the expansion of various functions

Map completes data type conversion

@Test
fun flowMapCase() = runBlocking {

    flow {
        emit(1)
        println("emit: 1")
    }.map {
        it * it
    }.collect {
        println("collect: $it")
    }

}

The above map has completed the data type conversion (power operation). As for how this is done?

public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
   return@transform emit(transform(value))
}

It can be seen from the above that the realization of map is by putting on another Flow clothes on the other parts of Flow, and then taking it off layer by layer when calling it.

flowOn completes thread conversion

@Test
fun flowOnIOCase() = runBlocking {

    flow {
        println("${Thread.currentThread().name}:emit: 1")
        emit(1)
    }.flowOn(Dispatchers.IO)
        .collect {
            println("${Thread.currentThread().name}:collect# $it")
        }
}

Output:

DefaultDispatcher-worker-1 @coroutine#2:emit: 1
main @coroutine#1:collect# 1

Explain that the emit thread is in the IO thread, because "flowOn(Dispatchers.IO)" is the operation of socketing a thread transfer in the original Flow:

public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T> {
    checkFlowContext(context)
    return when {
        context == EmptyCoroutineContext -> this
        this is ChannelFlow -> update(context = context)
        else -> ChannelFlowOperatorImpl(this, context = context)
    }
}

Guess you like

Origin blog.csdn.net/wsx1048/article/details/108471180