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.
Official document: poke
Chinese document: poke
The following describes the usage instructions of the stream:
- 1. Asynchronous flow
- 2. Stream builder
- Three, cold flow
- 4. Continuity of flow
- 5. Stream context
- 6. Collect the flow in the specified coroutine (launchIn)
- Seven, start the flow
- 8. Cancellation of stream
- Nine, stream cancellation detection
- 10. Back pressure treatment
- Eleven, stream operators
- 12. Stream exception handling
- Thirteen, the completion of the flow
- 14. Use of StateFlow and SharedFlow
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.
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
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