Coroutine of Android Kotlin - the use of asynchronous flow Flow

Dataflows are built on top of coroutines, which emit multiple values ​​sequentially, as opposed to suspending functions that return only a single value. Conceptually, a data stream is a sequence of data that can be processed computationally asynchronously. The emitted values ​​must be of the same type.

A dataflow consists of three entities:

  • Providers generate data that is added to the data stream. Dataflows can also generate data asynchronously thanks to coroutines.
  • (Optional) Mediators can modify values ​​sent to the dataflow, or modify the dataflow itself.
  • The consumer consumes the values ​​in the data stream.

insert image description here

Official document: poke
Chinese document: poke

The following describes the usage instructions of the stream:

1. Asynchronous flow

If, we know that the suspending function can return a single value asynchronously, and want to return multiple values ​​asynchronously, what should we do?

Let's use List first to see,

  • List
fun simple(): List<Int> = listOf(1, 2, 3)
 
fun main() {
    simple().forEach { value -> println(value) } 
}

print result

1
2
3

List#forEach can return multiple values, but not asynchronously

  • sequence

We delay 100 milliseconds to represent the calculation time,

fun simple(): Sequence<Int> = sequence { // 序列构建器
    for (i in 1..3) {
        Thread.sleep(100) // 假装我们正在计算
        yield(i) // 产生下一个值
    }
}

fun main() {
    simple().forEach { value -> println(value) } 
}

The above code, before each print, will delay 100 milliseconds. However, Thread.sleep is also blocking, not asynchronous.

  • suspend function

Above all, the calculation process will block the main thread running the code. When these values ​​are computed by asynchronous code, we can mark the function simple with the suspend modifier so that it does its work without blocking and returns the result as a list:

suspend fun simple(): List<Int> {
    delay(1000) // 假装我们在这里做了一些异步的事情
    return listOf(1, 2, 3)
}

fun main() = runBlocking<Unit> {
    simple().forEach { value -> println(value) } 
}

The above code will return multiple values ​​asynchronously after suspending. However, it is to return all the numbers directly, instead of returning one every 100 milliseconds, which is different from our expectations.

  • flow

Using the above code, we can only return all values ​​at once. In order to represent the value stream (stream) of asynchronous calculation, we can implement it through the Flow type

fun simple(): Flow<Int> = flow { // 流构建器
    for (i in 1..3) {
        delay(100) // 假装我们在这里做了一些有用的事情
        emit(i) // 发送下一个值
    }
}

fun main() = runBlocking<Unit> {
    // 启动并发的协程以验证主线程并未阻塞
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    // 收集这个流
    simple().collect { value -> println(value) } 
}

The above code can be executed without blocking the main thread. Print a number every wait 100ms, as expected

print result

I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3

Multiple values ​​can be returned asynchronously through the stream, and we can immediately think of a classic usage scenario: downloading files and getting download progress.

Using Flow is different from other methods before

  • A Flow type builder function named flow
  • Code inside a flow{} building block can hang
  • The function simple is no longer marked with the suspend modifier
  • Streams emit values ​​using the emit() function
  • Streams collect values ​​using the collect() function

2. Stream builder

Flows are constructed by the flowof or asFlow extension functions; collected by the collect function.

How streams are constructed:

  • The flowOf builder defines a flow that emits a fixed set of values
  • Use the .asFlow() extension function to convert various collections and sequences into flows

sample code

 fun flowFive() {
        launch {
			 flowOf("flowOne", "flowTwo", "flowThree")
			                    .onEach { delay(1000) }
			                    .collect { printLog("$it") }
			    

            (1..5).asFlow().collect {
                printLog("$it")
            }

        } 
    }

Three, cold flow

Flow is a cold flow similar to a sequence, the code in the flow builder is not run until the flow is collected (collect)

Look at the code below

    suspend fun simpleFlow() = flow<Int> {
        printLog("flow 开始创建")
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }

 fun flowThree() {
        launch {
            val flow = simpleFlow()
            printLog("开始逻辑")
            flow.collect {
                printLog("$it")
            }
        }
    }


print result:

开始逻辑
flow 开始创建
1
2
3

4. Continuity of flow

Continuity of flow:

  • Each individual collection of the stream is performed sequentially, unless special operators are used
  • Each transition operator from upstream to downstream processes each emitted value before handing it off to the final operator.

sample code


   
(1..5).asFlow()
    .filter {
        println("Filter $it")
        it % 2 == 0              
    }              
    .map { 
        println("Map $it")
        "string $it"
    }.collect { 
        println("Collect $it")
    }  


Results of the

Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5

5. Stream context

  • The collection of streams always happens in the context of the calling coroutine, and this property of streams is called context preservation
  • Code inside a flow{} builder must obey the context-preserving attribute and is not allowed to emit from other contexts
  • flowOn operator, this function is used to change the context in which the flow occurs

Change thread by flowOn

suspend fun simpleFlow2() = flow<Int> {
        printLog("flow 线程:${Thread.currentThread().name}")
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }.flowOn(Dispatchers.Default)

    fun flowSix() {
        launch {
            simpleFlow2().collect {
                printLog("$it,线程:${Thread.currentThread().name}")
            }
        }
    }

6. Collect the flow in the specified coroutine (launchIn)

Using launchIn instead of collect, we can launch the collection of streams in a separate coroutine. specified scope

  fun simpleFlow3() = (1..3).asFlow().onEach { delay(1000) }
            .flowOn(Dispatchers.Default)

    fun flowSeven() {
        launch {
            val job = simpleFlow3().onEach { printLog("value:$it,Thread:${Thread.currentThread().name}") }
                    .launchIn(CoroutineScope(SupervisorJob() + Dispatchers.IO))
            
            //这里可以取消
            job.cancel()
        }
        
    }


Seven, start the flow

It's straightforward to use streams to represent asynchronous events from some source.

In this case, we need a function like a listener (addEventListener), which registers a piece of responsive code to handle the upcoming event and continue with further processing. The onEach operator can fill that role. However, onEach is a transition operator. We also need a terminal operator to collect streams. Otherwise just calling onEach has no effect.

If we use the collect terminal operator after onEach, then the following code will wait until the stream is collected:

// 模仿事件流
fun events(): Flow<Int> = (1..3).asFlow().onEach { delay(100) }

fun main() = runBlocking<Unit> {
    events()
        .onEach { event -> println("Event: $event") }
        .collect() // <--- 等待流收集
    println("Done")
}  

print result

Event: 1
Event: 2
Event: 3
Done

8. Cancellation of stream

Streams employ the same cooperative cancellation as coroutines. As usual, stream collection can be canceled while the stream is suspended in a cancelable suspending function (eg, delay )

Start the stream in the coroutine, if the coroutine is canceled, the stream will also be cancelled.



    suspend fun simpleFlow4() = flow<Int> {
        printLog("flow 开始创建")
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }
    fun flowEight() {
        launch {
            //在超时操作时,取消
            withTimeoutOrNull(2100){
                simpleFlow4().collect{
                    printLog("value: $it")
                }
            }
            printLog("执行完成")
        }
    }

Nine, stream cancellation detection

  • For convenience, the flow builder performs an additional ensureActive check on each emitted value for cancellation, which means that frequent loops emitted from flow{} are cancelable.
  • For performance reasons, most other stream operations do not perform additional cancellation detection themselves, and in the case of a coroutine in a busy loop, cancellation must be explicitly detected
  • Do this with the cancellable operator
  suspend fun simpleFlow5() = flow<Int> {
        printLog("flow 开始创建")
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }

    fun flowNine() {
        launch {
            simpleFlow5().collect {
                printLog("value: $it")
                if (it == 2) {
                    cancel()
                }
            }
        }
    }




Cancellation was unsuccessful during busy times

   fun flow10() {
        launch {
            (1..9).asFlow().collect {
                printLog("value: $it")
                if (it == 5) {
                    cancel()
                }
            }
        }
    }

If, we also need to detect whether to cancel in this case. need to use cancellable


 fun flow10() {
        launch {
            (1..9).asFlow().cancellable().collect {
                printLog("value: $it")
                if (it == 5) {
                    cancel()
                }
            }
        }
    }


10. Dealing with back pressure

Back pressure refers to a strategy to tell the upstream observer to slow down the sending speed when the observer sends events much faster than the observer can process them in an asynchronous scenario.

  • buffer(), emits elements in a concurrent run stream
  • conflate(), merge emission items, do not process each value (optimize consumers)
  • collectLatest(), cancels and re-emits the last value
  • The flowOn operator uses the same buffering mechanism when the CoroutineDispatcher must be changed, but the buffer() function explicitly requests buffering without changing the execution context (thread)

When the producer speed > consumer, back pressure is generated, see the code

    suspend fun simpleFlow6() = flow<Int> {
        printLog("flow 开始创建,Thread:${Thread.currentThread().name}")
        for (i in 1..3) {
            //生产出来,需要100
            delay(100)
            emit(i)
        }
    }

    fun flow11() {

        launch {
            val time = measureTimeMillis {
                simpleFlow6().collect {
                    //消耗需要200。这样就产生了背压
                    delay(200)
                    printLog("value: $it,Thread:${Thread.currentThread().name}")
                }
            }
            printLog("总耗时:$time")
        }

    }

The above one, the producer, emits data every 100 milliseconds. Consumers (collect) need 200 milliseconds to consume data once. This creates back pressure.

  • Use buffers, optimize backpressure
  fun flow11() {

        launch {
            val time = measureTimeMillis {
                simpleFlow6().buffer(10).collect {
                    //消耗需要200毫秒。这样就产生了背压
                    delay(200)
                    printLog("value: $it,Thread:${Thread.currentThread().name}")
                }
            }
            printLog("总耗时:$time")
        }

    }

  • Use flowOn to switch threads, process
fun flow11() {

        launch {
            val time = measureTimeMillis {
                simpleFlow6().flowOn(Dispatchers.Default).collect {
                    //消耗需要200。这样就产生了背压
                    delay(200)
                    printLog("value: $it,Thread:${Thread.currentThread().name}")
                }
            }
            printLog("总耗时:$time")
        }

    }

  • Use conflate() to combine emission items without processing each value
 fun flow11() {

        launch {
            val time = measureTimeMillis {
                simpleFlow6()
//                        .buffer(20)
//                        .flowOn(Dispatchers.Default)
                        .conflate()
                        .collect {
                    //消耗需要200。这样就产生了背压
                    delay(200)
                    printLog("value: $it,Thread:${Thread.currentThread().name}")
                }
            }
            printLog("总耗时:$time")
        }

    }

In this way, the intermediate value may be lost and the latest value is used directly.

  • Process with collectLatest
 fun flow11() {

        launch {
            val time = measureTimeMillis {
                simpleFlow6()
//                        .buffer(20)
//                        .flowOn(Dispatchers.Default)
                        .conflate()
                        .collectLatest {
                    //消耗需要200。这样就产生了背压
                    delay(200)
                    printLog("value: $it,Thread:${Thread.currentThread().name}")
                }
            }
            printLog("总耗时:$time")
        }

    }


only use the latest value

Eleven, stream operators

11.1 Conversion operators

  • map operator

https://kotlinlang.org/docs/flow.html#intermediate-flow-operators

suspend fun performRequest(request: Int): String {
    delay(1000) // imitate long-running asynchronous work
    return "response $request"
}

fun main() = runBlocking<Unit> {
    (1..3).asFlow() // a flow of requests
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

print result

response 1
response 2
response 3

  • Transform operator

Among the stream conversion conversion symbols, the most common one is Transform. It can be used to simulate simple transformations such as maps and filters, as well as to implement more complex transformations. Using the Transform operator, we can emit any value any number of times.

code example

(1..3).asFlow() // a flow of requests
    .transform { request ->
        emit("Making request $request") 
        emit(performRequest(request)) 
    }
    .collect { response -> println(response) }

print result

Making request 1
response 1
Making request 2
response 2
Making request 3
response 3

11.2 Limit-size operators

Size limit intermediate operators such as take will cancel execution of the stream when the corresponding limit is reached. Cancellation in coroutines is always performed by throwing an exception, so that in case of cancellation, all resource management functions (such as try{...}finally{...} blocks) behave normally:

  • take operator

fun numbers(): Flow<Int> = flow {
    try {                          
        emit(1)
        emit(2) 
        println("This line will not execute")
        emit(3)    
    } finally {
        println("Finally in numbers")
    }
}

fun main() = runBlocking<Unit> {
    numbers() 
        .take(2) // take only the first two
        .collect { value -> println(value) }
} 

// print the result

1
2
Finally in numbers

11.3 Terminal operator

The terminal operator is a suspend function on a stream that initiates stream collection. collect is the most basic terminal operator, but there are other terminal operators that are more convenient to use:

  • Converted to various collections, such as toList and toSet.
  • An operator that takes the first value and ensures that the stream emits a single value.
  • Use reduce and fold to reduce a stream to a single value.

code example

val sum = (1..5).asFlow()
    .map { it * it } // 数字 1 至 5 的平方                        
    .reduce { a, b -> a + b } // 求和(末端操作符)
println(sum)

print result

55

11.4 Composition operators

  • Zip
    Like the Sequence.zip extension function in the Kotlin standard library, streams have a zip operator for combining related values ​​from two streams:
val nums = (1..3).asFlow() // 数字 1..3
val strs = flowOf("one", "two", "three") // 字符串
nums.zip(strs) { a, b -> "$a -> $b" } // 组合单个字符串
    .collect { println(it) } // 收集并打印

print result

1 -> one
2 -> two
3 -> three

  • Combine
    may need to perform computations when a stream represents the latest value of a variable or operation, which depends on the latest value of the corresponding stream and needs to be recomputed whenever the upstream stream produces a value. The corresponding operator is combine

For example, if the numbers in the previous example were updated every 300 milliseconds, but the strings were updated every 400 milliseconds, then using the zip operator to merge them would still produce the same result, despite printing the result every 400 milliseconds:

We use the onEach transition operator in this example to delay each element emission and make the flow more declarative and concise.

val nums = (1..3).asFlow().onEach { delay(300) } // 发射数字 1..3,间隔 300 毫秒
val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒发射一次字符串
val startTime = System.currentTimeMillis() // 记录开始的时间
nums.zip(strs) { a, b -> "$a -> $b" } // 使用“zip”组合单个字符串
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    }

Below, we use combine instead of zip

val nums = (1..3).asFlow().onEach { delay(300) } // 发射数字 1..3,间隔 300 毫秒
val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒发射一次字符串
val startTime = System.currentTimeMillis() // 记录开始的时间
nums.combine(strs) { a, b -> "$a -> $b" } // 使用“combine”组合单个字符串
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

We get a completely different output, where one line is printed for each emission in the nums or strs streams:

1 -> one at 452 ms from start
2 -> one at 651 ms from start
2 -> two at 854 ms from start
3 -> two at 952 ms from start
3 -> three at 1256 ms from start

11.5 The flatten operator

There are:

  • flatMapConcat
  • flatMapMerge
  • flatMapLatest

A stream represents a sequence of values ​​received asynchronously, so it's easy to run into situations where each value triggers a request for another sequence of values. For example, we could have the following function that returns two streams of strings separated by 500 milliseconds:

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // 等待 500 毫秒
    emit("$i: Second")    
}

Now, if we have a flow with three integers and call requestFlow for each integer like this

(1..3).asFlow().map { requestFlow(it) }

Then we get a flow containing flows (Flow<Flow>), which needs to be flattened into a single flow for further processing. Both collections and sequences have flatten and flatMap operators to do this. However, due to the asynchronous nature of streams, different flattening modes are required, for which a series of stream flattening operators exist.

flatMapConcat

The join mode is implemented by the flatMapConcat and flattenConcat operators. They are the closest analogs of the corresponding sequence operators. They start collecting the next value before waiting for the inner stream to complete

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapConcat { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

print result

1: First at 121 ms from start
1: Second at 622 ms from start
2: First at 727 ms from start
2: Second at 1227 ms from start
3: First at 1328 ms from start
3: Second at 1829 ms from start

flatMapMerge

Another flattening pattern is to collect all incoming streams concurrently and merge their values ​​into a single stream so that values ​​can be emitted as quickly as possible. It is implemented by the flatMapMerge and flattenMerge operators. They both receive an optional concurrency parameter (by default, it is equal to DEFAULT_CONCURRENCY ) to limit the number of concurrently collected streams.

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapMerge { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

print result

1: First at 136 ms from start
2: First at 231 ms from start
3: First at 333 ms from start
1: Second at 639 ms from start
2: Second at 732 ms from start
3: Second at 833 ms from start

flatMapLatest

Similar to the collectLatest operator, there is also a corresponding "latest" flattening mode, which cancels the collection of previous streams as soon as a new stream is emitted. This is achieved by the flatMapLatest operator.

val startTime = System.currentTimeMillis() // 记录开始时间
(1..3).asFlow().onEach { delay(100) } // 每 100 毫秒发射一个数字 
    .flatMapLatest { requestFlow(it) }                                                                           
    .collect { value -> // 收集并打印
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

print result

1: First at 142 ms from start
2: First at 322 ms from start
3: First at 425 ms from start
3: Second at 931 ms from start

Note that flatMapLatest cancels all code in the block ( { requestFlow(it) } in this example) when a new value arrives. It doesn't make a difference in this particular example, since the call to requestFlow itself is fast, there is no hang, and therefore no cancellation. However, if we were to call suspending functions such as delay inside the block, this would be manifested.

12. Stream exception handling

How to handle when an exception is thrown by an emitter or code inside an operator:

  • try/catch processing
  • catch function processing

The try/catch block is a commonly used exception capture function in Kotlin. In the processing of the stream, we can also use the special catch() function to handle the exceptions that occur during the entire process of the stream from emission to collection.

try/catch processing

Exception handling occurs during collector. First, we can use try/catch in Kotlin to handle

   suspend fun simpleFlow7() = flow<Int> {
        for (i in 1..3) {
            //生产出来,需要100
            delay(100)
            emit(i)
        }
    }

    fun flow12() {
        launch {
            try {
                simpleFlow6()
                        .collect {
                            //消耗需要200。这样就产生了背压
                            printLog("value: $it")
							//如果值小于1,抛出异常
                            check(it <= 1) { "result: $it" }
                        }
            } catch (e: Throwable) {
                printLog("Exception:$e")
            }
        }

    }

As you can see here, the exception is caught

However, a flow must be transparent to exceptions, ie emitting values ​​in a try/catch block inside a flow { ... } builder is a violation of exception transparency.

catch function processing

Emitters can use the catch operator to preserve the transparency of this exception and allow its exception handling to be encapsulated. The code block of the catch operator can analyze exceptions and react to them in different ways depending on the exception caught:

  • Exceptions can be rethrown using throw.
  • You can use emit in the catch code block to convert the exception into a value and emit it.
  • The exception can be ignored, or logged, or handled with some other code.

code example

fun flow13() {
       launch {
           flow<Int> {
               emit(2)
               //主动抛出异常
               throw NullPointerException("数据异常")
           }.catch {_ -> emit(-1) } //出现异常后,重新发送一个数据过去
                   .flowOn(Dispatchers.IO)
                   .collect { printLog("$it") }
       }
    }

Here, an exception is actively thrown, and after being caught by the catch function, a data is resent to the end.

transparent capture

The catch transition operator follows exception transparency and only catches upstream exceptions (exceptions upstream of the catch operator, but not below it). If the collect { ... } block (under the catch) throws an exception, the exception escapes

code example

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .catch { e -> println("Caught $e") } // 不会捕获下游异常
        .collect { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
}     

The above code despite the catch function. However, because the exception occurs at the end of collect (appears after the catch function), this exception cannot be caught.

The solution: declarative capture

We can combine the declarative nature of the catch operator with the expectation of handling all exceptions by moving the code block of the terminal (collect) operator into onEach and placing it before the catch operator. Collection of the stream must be triggered by calling collect() with no arguments

Simply put, if we want the catch function to handle all exceptions. we need to:

  • We need to put the code in the collect() function into onEach (before the catch function) to execute
  • Call the collect() function without parameters to collect the stream
simple()
    .onEach { value ->
        check(value <= 1) { "Collected $value" }                 
        println(value) 
    }
    
    .catch { e -> println("Caught $e") }
    .collect()

print result

Emitting 1
1
Emitting 2
Caught java.lang.IllegalStateException: Collected 2

In this way, all exceptions can be caught without explicit use of try/catch blocks.

Thirteen, the completion of the flow

When a flow collection is complete (normal or exceptional), it may need to perform an action.

  • imperative finally block, processing
  • Declaratively handle the onCompletion function

imperative finally block

In addition to try/catch, collectors can also use finally blocks to perform an action when collect completes.

fun simple(): Flow<Int> = (1..3).asFlow()

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value -> println(value) }
    } finally {
        println("Done")
    }
} 

This code, after printing the simple stream, will print done in finally

1
2
3
Done

Declaratively handle the onCompletion function

Streams have an onCompletion operator, which is called when the stream is fully collected

sample code

simple()
    .onCompletion { println("Done") }
    .collect { value -> println(value) }

The print result is the same as above

The main advantage of onCompletion is that its lambda expression's nullable parameter Throwable can be used to determine whether the stream collection completed normally or with an exception.

Below, we use exception completion to demonstrate

fun simple(): Flow<Int> = flow {
    emit(1)
    throw RuntimeException()
}

fun main() = runBlocking<Unit> {
    simple()
        .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
        .catch { cause -> println("Caught exception") }
        .collect { value -> println(value) }
}

print result

1
Flow completed exceptionally
Caught exception

Here, you can see that the code of the onCompletion function is executed.

The onCompletion operator, unlike catch, does not handle exceptions. As we can see in the previous sample code, exceptions still flow downstream. It will be provided to the following onCompletion operator and can be handled by the catch operator.



Another difference from the catch operator is that onCompletion observes all exceptions and only receives a null exception if the upstream stream completed successfully (without cancellation or failure).

sample code

fun simple(): Flow<Int> = (1..3).asFlow()

fun main() = runBlocking<Unit> {
    simple()
        .onCompletion { cause -> println("Flow completed with $cause") }
        .collect { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
}

print result

1
Flow completed with java.lang.IllegalStateException: Collected 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2

We can see that cause is not null on completion because the stream was aborted due to a downstream exception

14. StateFlow and SharedFlow

StateFlow and SharedFlow are Flow APIs that allow dataflows to optimally emit state updates and emit values ​​to multiple consumers.

Official address: click

StateFlow

StateFlow is a state container-style observable data flow that can send current state updates and new state updates to the collector. You can also read the current state value through its value property.
It can only have one observer can get the data.

Unlike cold flows built using the flow builder, StateFlow is hot: collecting data from a flow does not trigger any provider code. A StateFlow is always alive and in memory, and is only eligible for garbage collection if no other references to it are involved in the garbage collection root.

We create a page with 2 buttons in it, and modify the value of textView through "+", "-"
Sample code:

Activity

class FlowTestActivity : AppCompatActivity() {
    private val textView by lazy {
        findViewById<TextView>(R.id.tv_content)
    }
    private val viewModel by viewModels<FlowTestViewModel>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flow_test)
        //启动协程
        lifecycleScope.launchWhenCreated {
        	//通过flow来收集数据
            viewModel.flowNumber.collect {
                textView.text = "$it"
            }
        }
    }
    //点击按钮+
    fun onNumberPlus(_: View) {
        viewModel.numPlus()
    }
    fun onNumberMinus(_: View) {
        viewModel.numMinus()
    }
}

ViewModel

class FlowTestViewModel : ViewModel() {
    val flowNumber = MutableStateFlow(0)

    fun numPlus() {
        flowNumber.value++
    }

    fun numMinus() {
        flowNumber.value--
    }
}

Click the button, we found that the function is realized.

insert image description here

If you have used LiveData. So far, we found that the use of StateFlow and LiveData is very similar. So what's the difference between them.

StateFlow 和 LiveData

StateFlow and LiveData share similarities. Both are observable data container classes.

How they differ:

  • StateFlow requires an initial state to be passed to the constructor, LiveData does not.
  • LiveData.observe() automatically unregisters the consumer when the View enters the STOPPED state, but collecting data from StateFlow or any other data flow does not stop automatically. To achieve the same behavior, you need to collect data flow from Lifecycle.repeatOnLifecycle block.

The repeatOnLifecycle API is only available in the androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 library and higher.

Cold flow to hot flow (ShareIn)

StateFlows are hot dataflows that stay in memory as long as the flow is collected, or any other reference to it exists in the garbage collection root. You can use the shareIn operator to turn cold data streams into hot data streams.

To switch from cold flow to hot flow, the following conditions must be met:

  • CoroutineScope for shared data flow. This scoped function should outlive any consumers, keeping the shared data stream alive long enough.
  • The number of data items to replay to each new collector.
  • "Startup" conduct policy.

code example

class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

In this example, the latestNews stream replays the last emitted data item to the new collector, which will remain active as long as the externalScope is active and there are active collectors. The SharingStarted.WhileSubscribed() "start" policy will keep the upstream provider active while there are active subscribers. Other start policies can be used, such as SharingStarted.Eagerly to start the provider immediately, and SharingStarted.Lazily to start sharing data after the first subscriber comes along and keep the stream alive forever.

SharedFlow

SharedFlow emits data to all consumers from which it collects values. It is very similar to BroadcastChannel (broadcast channel), which belongs to a one-to-many relationship.

Seeing this, the first scene we thought of: Is it very suitable for ViewPager+Fragment to share data?

In order to reduce the amount of code, we implement this function through 3 TextViews in one page.

Activity

class FlowTestActivity : AppCompatActivity() {
    private val textView by lazy {
        findViewById<TextView>(R.id.tv_content)
    }
    private val textView2 by lazy {
        findViewById<TextView>(R.id.tv_content_2)
    }
    private val textView3 by lazy {
        findViewById<TextView>(R.id.tv_content_3)
    }
    private val viewModel by viewModels<FlowTestViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flow_test)

        lifecycleScope.launchWhenCreated {
            ShareFlowNumEvent.shareEvent.collect {
                textView.text = "${it.num}"
            }
        }
        lifecycleScope.launchWhenCreated {
            ShareFlowNumEvent.shareEvent.collect {
                textView2.text = "${it.num}"
            }
        }
        lifecycleScope.launchWhenCreated {
            ShareFlowNumEvent.shareEvent.collect {
                textView3.text = "${it.num}"
            }
        }
    }
    fun onNumberPlus(view: View) {
        viewModel.sharePlus()
    }

    fun onNumberMinus(view: View) {
        viewModel.shareMinus()
    }
}

Here, the 2 buttons in the above example are also used, and 3 textViews are used.

Create sharedFlow below, we use a singleton, after all, it is shared

object ShareFlowNumEvent {
    val shareEvent = MutableSharedFlow<NumData>()

    suspend fun shareData(data: NumData) {
        shareEvent.emit(data)
    }
}
data class NumData(val num: Int = 0)

Create a singleton and a data wrapper class

ViewModel

class FlowTestViewModel : ViewModel() {
    private var job: Job? = null
    fun sharePlus() {
        job = viewModelScope.launch(Dispatchers.IO) {
            ShareFlowNumEvent.shareData(NumData(Random.nextInt(10)))
        }
    }

    fun shareMinus() {
        job?.cancel()
    }
}

By clicking the page button, call sharePlus, and send (emit) the data flow through ShareFlowNumEvent.shareData.

On the UI page, collect streams through collect.

From the picture below, we can know that all three TextViews have received data

insert image description here

References:

https://developer.android.google.cn/kotlin/flow?hl=zh-cn

https://kotlinlang.org/docs/flow.html#terminal-flow-operators

https://www.kotlincn.net/docs/reference/coroutines/flow.html

https://juejin.cn/post/7007602776502960165#comment

Guess you like

Origin blog.csdn.net/ecliujianbo/article/details/128320519
Recommended