[Kotlin Tutorials 19] Kotlin Flows, SharedFlow and StateFlow in Android
Kotlin Flows
本文包含的内容:
- Flow是什么, 基本概念和用法.
- Flow的不同类型, StateFlow和SharedFlow比较.
- Flow在Android中的使用
- 安全收集.
- 操作符
stateIn,shareIn的用法和区别.
本文被收录在集合中: https://github.com/mengdd/KotlinTutorials
Coroutines Flow Basics
Flow是什么
Flow可以按顺序发送多个值, 概念上是一个数据流, 发射的值必须是同一个类型.
Flow使用suspend方法来生产/消费值, 数据流可以做异步计算.
几个基本知识点:
- 创建flow: 通过flow builders
- Flow数据流通过
emit()来发射元素. - 可以通过各种操作符对flow的数据进行处理. 注意中间的操作符都不会触发flow的数据发送.
- Flow默认是cold flow, 即需要通过被观察才能激活, 最常用的操作符是
collect(). - Flow的
CoroutineContext, 不指定的情况下是collect()的CoroutineContext, 如果想要更改, 用flowOn
改之前的.
关于Flow的基本用法, 19年底写的这篇coroutines flow in Android可以温故知新.
Flow的操作符
一个Flow操作符的可视化小网站: FlowMarbles.
Flow的不同类型
SharedFlow and StateFlow
应用程序里比较常用的类型是SharedFlow和StateFlow.
Android官方有一篇专门的文档来介绍二者: StateFlow and SharedFlow
StateFlow继承于SharedFlow, SharedFlow继承于Flow.
基本关系如下:

Flow
基类. Cold.
Flow的两大特性: Context preservation; Exception transparency.SharedFlow
继承Flow, 是一种hot flow, 所有collectors共享它的值, 永不终止, 是一种广播的方式.
一个shared flow上的活跃collector被叫作subscriber.
在sharedFlow上的collect call永远不会正常complete, 还有Flow.launchIn.
可以配置replay and buffer overflow strategy.
如果subscriber suspend了, sharedflow会suspend这个stream, buffer这个要发射的元素, 等待subscriber resume.
Because onBufferOverflow is set with BufferOverflow.SUSPEND, the flow will suspend until it can deliver the event to all subscribers.
默认参数:
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
total buffer是: replay + extraBufferCapacity.
如果total buffer是0, 那么onBufferOverflow只能是onBufferOverflow = BufferOverflow.SUSPEND.
关于reply和buffer, 这个文章
有详细的解释, 并且配有动图.
- StateFlow
继承SharedFlow, hot flow, 和是否有collector收集无关, 永不complete.
可以通过value属性访问当前值.
有conflated特性, 会跳过太快的更新, 永远返回最新值.
Strong equality-based conflation: 会通过equals()来判断值是否发生改变, 如果没有改变, 则不会通知collector.
因为conflated的特性, StateFlow赋值的时候要注意使用不可变的值.
cold vs hot
cold stream 可以重复收集, 每次收集, 会对每一个收集者单独开启一次.
hot stream 永远发射不同的值, 和是否有人收集无关, 永远不会终止.
StateFlow vs SharedFlow
共性:
StateFlow和SharedFlow永远都不会停止. 不能指望它们的onCompletionCallback.
不同点:
StateFlow可以通过value属性读到最新的值, 但SharedFlow却不行.StateFlow是conflated: 如果新的值和旧的值一样, 不会传播.SharedFlow需要合理设置buffer和replay策略.
互相转换:
SharedFlow用了distinctUntilChanged以后变成StateFlow.
// MutableStateFlow(initialValue) is a shared flow with the following parameters:
val shared = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // emit the initial value
val state = shared.distinctUntilChanged() // get StateFlow-like behavior
RxJava的等价替代:
PublishSubject->SharedFlow.BehaviorSubject->StateFlow.
Use Flow in Android
发送事件(Event或Effects): SharedFlow
因为SharedFlow没有conflated特性, 所以适合发送事件, 即便值变化得快也是每个都发送.
private val _sharedViewEffects = MutableSharedFlow<SharedViewEffects>() // 1
val sharedViewEffects = _sharedViewEffects.asSharedFlow() // 2
这里用了asSharedFlow来创建一个ReadonlySharedFlow.
SharedFlow发射元素有两个方法:
emit: suspend方法.tryEmit: 非suspend方法.
因为tryEmit是非suspend的, 适用于有buffer的情况.
保存暴露UI状态: StateFlow
StateFlow是一个state-holder, 可以通过value读到当前状态值.
一般会有一个MutableStateFlow类型的Backing property.
StateFlow是hot的, collect并不会触发producer code.
当有新的consumer时, 新的consumer会接到上次的状态和后续的状态.
使用StateFlow时, 发射新元素只需要赋值:
mutableState.value = newState
注意这里新值和旧的值要equals判断不相等才能发射出去.
StateFlow vs LiveData
StateFlow和LiveData很像.
StateFlow和LiveData的相同点:
- 永远有一个值.
- 只有一个值.
- 支持多个观察者.
- 在订阅的瞬间, replay最新的值.
有一点点不同:
StateFlow需要一个初始值.LiveData会自动解绑, flow要达到相同效果, collect要在Lifecycle.repeatOnLifecycle里.
Flow的安全收集
关于收集Flow的方法, 主要还是关注一下生命周期的问题, 因为SharedFlow和StateFlow都是hot的.
在这个文章里有详细的讨论: A safer way to collect flows from Android UIs
在UI层收集的时候注意要用repeatOnLifecycle:
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
//...
// Start a coroutine in the lifecycle scope
lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// Note that this happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
这个文章里有个扩展方法也挺好的:
class FlowObserver<T> (
lifecycleOwner: LifecycleOwner,
private val flow: Flow<T>,
private val collector: suspend (T) -> Unit
) {
private var job: Job? = null
init {
lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver {
source: LifecycleOwner, event: Lifecycle.Event ->
when (event) {
Lifecycle.Event.ON_START -> {
job = source.lifecycleScope.launch {
flow.collect { collector(it) }
}
}
Lifecycle.Event.ON_STOP -> {
job?.cancel()
job = null
}
else -> { }
}
})
}
}
inline fun <reified T> Flow<T>.observeOnLifecycle(
lifecycleOwner: LifecycleOwner,
noinline collector: suspend (T) -> Unit
) = FlowObserver(lifecycleOwner, this, collector)
inline fun <reified T> Flow<T>.observeInLifecycle(
lifecycleOwner: LifecycleOwner
) = FlowObserver(lifecycleOwner, this, {})
看了一下官方的repeatOnLifecycle其实大概也是这个意思:
public suspend fun Lifecycle.repeatOnLifecycle(
state: Lifecycle.State,
block: suspend CoroutineScope.() -> Unit
) {
require(state !== Lifecycle.State.INITIALIZED) {
"repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state."
}
if (currentState === Lifecycle.State.DESTROYED) {
return
}
// This scope is required to preserve context before we move to Dispatchers.Main
coroutineScope {
withContext(Dispatchers.Main.immediate) {
// Check the current state of the lifecycle as the previous check is not guaranteed
// to be done on the main thread.
if (currentState === Lifecycle.State.DESTROYED) return@withContext
// Instance of the running repeating coroutine
var launchedJob: Job? = null
// Registered observer
var observer: LifecycleEventObserver? = null
try {
// Suspend the coroutine until the lifecycle is destroyed or
// the coroutine is cancelled
suspendCancellableCoroutine<Unit> { cont ->
// Lifecycle observers that executes `block` when the lifecycle reaches certain state, and
// cancels when it falls below that state.
val startWorkEvent = Lifecycle.Event.upTo(state)
val cancelWorkEvent = Lifecycle.Event.downFrom(state)
val mutex = Mutex()
observer = LifecycleEventObserver { _, event ->
if (event == startWorkEvent) {
// Launch the repeating work preserving the calling context
launchedJob = this@coroutineScope.launch {
// Mutex makes invocations run serially,
// coroutineScope ensures all child coroutines finish
mutex.withLock {
coroutineScope {
block()
}
}
}
return@LifecycleEventObserver
}
if (event == cancelWorkEvent) {
launchedJob?.cancel()
launchedJob = null
}
if (event == Lifecycle.Event.ON_DESTROY) {
cont.resume(Unit)
}
}
this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)
}
} finally {
launchedJob?.cancel()
observer?.let {
this@repeatOnLifecycle.removeObserver(it)
}
}
}
}
}
既然官方已经推出了, 我们就用官方的repeatOnLifecycle方法吧.
shareIn和stateIn
前面提过这两个操作符是用来做flow转换的:
shareIn可以保证只有一个数据源被创造, 并且被所有collectors收集.
比如:
class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}
WhileSubscribed这个策略是说, 当无人观测时, 上游的flow就被取消.
实际使用时可以用WhileSubscribed(5000), 让上游的flow即便在无人观测的情况下, 也能继续保持5秒.
这样可以在某些情况(比如旋转屏幕)时避免重建上游资源, 适用于上游资源创建起来很expensive的情况.
如果我们的需求是, 永远保持一个最新的cache值.
class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}
Flow.stateIn将会缓存最后一个值, 并且有新的collector时, 将这个最新值传给它.
shareIn, stateIn使用注意事项
永远不要在方法里面调用shareIn和stateIn, 因为方法每次被调用, 它们都会创建新的流.
这些流没有被复用, 会存在内存里面, 直到scope被取消或者没有引用时被GC.
推荐的使用方式是在property上用:
class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// DO NOT USE shareIn or stateIn in a function like this.
// It creates a new SharedFlow/StateFlow per invocation which is not reused!
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())
// DO USE shareIn or stateIn in a property
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}
StateFlow使用总结
从ViewModel暴露数据到UI, 用StateFlow的两种方式:
- 暴露一个StateFlow属性, 用
WhileSubscribed加上一个timeout.
class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
- 用
repeatOnLifecycle收集.
onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}
其他的组合都会保持上游的活跃, 浪费资源:
- 用
WhileSubscribed暴露属性, 在lifecycleScope.launch/launchWhenX里收集. - 通过
Lazily/Eagerly暴露, 用repeatOnLifecycle收集.
References
- Kotlin flows on Android
- StateFlow and SharedFlow
- A safer way to collect flows from Android UIs
- Things to know about Flow’s shareIn and stateIn operators
- Shared flows, broadcast channels
- Kotlin SharedFlow or: How I learned to stop using RxJava and love the Flow
- Migrating from LiveData to Kotlin’s Flow
- Substituting Android’s LiveData: StateFlow or SharedFlow?
- Learning State & Shared Flows with Unit Tests
- Reactive Streams on Kotlin: SharedFlow and StateFlow
- Reading Coroutine official guide thoroughly — Part 0
[Kotlin Tutorials 19] Kotlin Flows, SharedFlow and StateFlow in Android的更多相关文章
- Kotlin Tutorials系列文章
Kotlin Tutorials系列文章 想写一个有实用价值的Kotlin笔记, 让一线开发一看就懂, 看完就能上手. 当然官方文档更有参考价值了. 这个系列相对于官方文档的大而全来说, 最主要优势是 ...
- 【Kotlin】初识Kotlin(二)
[Kotlin]初识Kotlin(二) 1.Kotlin的流程控制 流程控制是一门语言中最重要的部分之一,从最经典的if...else...,到之后的switch,再到循环控制的for循环和while ...
- 【Kotlin】初识Kotlin之面向对象
[Kotlin]初识Kotlin之面向对象 1.类 在Kotlin中,类用关键字class来定义 如果一个类具有类体,那么需要使用{ }来写类体内容,如果不需要类体,那么只需要定义类名就可以了 // ...
- Kotlin入门(19)Android的基础布局
线性布局线性布局LinearLayout是最常用的布局,顾名思义,它下面的子视图像是用一根线串了起来,所以其内部视图的排列是有顺序的,要么从上到下垂直排列,要么从左到右水平排列.排列顺序只能指定一维方 ...
- 初试kotlin:用Kotlin开发桌面/CommandLine 工具
既然kotlin是google和jetbrain联合搞的,开发环境不用说了肯定是Intellij Idea了. 先创建一个kotlin项目. 先来一个HelloWorld package com.xi ...
- Kotlin的属性委托:无上下文情况下Android的赋值(KAD 15)
作者:Antonio Leiva 时间:Mar 9, 2017 原文链接:https://antonioleiva.com/property-delegation-kotlin/ 如我们在前面文章中读 ...
- 序章:为什么学习使用kotlin、及kotlin的一些碎碎念
为什么使用kotlin? 当然是因为项目目前的开发语言是kotlin啊! 一方面是想能够尽快适应项目,另一方面,kotlin这门语言独特的语法,确实很吸引我,也让我意识到java代码在某些程度上的繁琐 ...
- Java调用Kotlin事项及Kotlin反射初步
继续来研究Java调用Kotlin的一些东东. @Throws注解: 我们知道在Kotlin中是不存在checked exception的,而在Java中是存在的,那..如果从Java来调用Kotli ...
- JavaEE Tutorials (19) - Web应用安全入门
19.1Web应用安全概述29519.2保护Web应用安全296 19.2.1指定安全约束297 19.2.2指定认证机制300 19.2.3在部署描述文件中指定认证机制302 19.2.4声明安全角 ...
随机推荐
- springMVC-3-获取参数
RequestMapping修饰类 源码: 根据源码可以知道,requestmapping既可以修饰方法也可以修饰类 @Target({ElementType.METHOD, ElementType. ...
- spring-2-AOP
AOP(面向切面编程) 面向切面编程, 即利用AOP可以对业务逻辑的各个部分进行隔离, 从而使得业务逻辑各个部分之间的耦合度降低, 提高程序的可重用性, 同时提高了开发的效率. 不通过修改源代码,通过 ...
- 【对线面试官】CountDownLatch和CyclicBarrier的区别
<对线面试官>系列目前已经连载31篇啦,这是一个讲人话面试系列 [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 &am ...
- windows使用nvm管理node不同版本
最近项目需要升级,新技术需要的node版本较高,而新node不兼容旧版本node,而原项目仍需要继续维护,所以就需要在本地有多个版本的node,基本原理是在环境配置中修改系统变量node的版本文件夹路 ...
- CSS 四种样式表 六种规则选择器 五种常用样式属性
新的html程序要在VS中编写了,在vs中安装ASP.NET和Web开发,并用ASP.NET Web 应用程序(.NETFramework)创建一个网页程序.添加一个html页 后面的代码都是在htm ...
- 第十篇 -- 学习C++宝典2005版
最近看了C++宝典,看时间是2005的,对于里面的程序自己也进行了编写,由于时间过久,可能有些函数的用法发生了改变,自己也对其进行了修改,用VS2017可以编译通过. 前四章学习内容 CPlusPlu ...
- (opencv10)膨胀和侵蚀(Dilation与Erosion)
(opencv10)膨胀和侵蚀(Dilation与Erosion) 图像形态学操作 图像形态学操作-基于形状的一系列图像处理操作的合集,主要是基于集合论基础上的形态学数学 形态学有四个基本操作:腐蚀, ...
- Xshell 打开时,初始运行卡慢优化方法
我使用的是Xshell 6免费版,有需要的同学可以去这个地址下载:https://www.netsarang.com/download/down_form.html?code=622 一开始安装完Xs ...
- js 跨域请求失败
注:错误返回:Failed to load http://xxxxxxxxxxx: No 'Access-Control-Allow-Origin' header is present on the ...
- 【硬核】MMU是如何完成地址翻译的
目录 1. 什么是虚拟内存? 2. 虚拟内存的作用 3. 虚拟内存与物理内存 3.1 CPU存取数据 3.2 物理地址常用术语 3.3 虚拟地址常用术语 3.4 页表常用术语 3.5 页命中/缺页 4 ...