[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声明安全角 ...
随机推荐
- Optional 的使用会导致性能下降吗?
几天前,我在论坛上发了一篇关于Optional 的文章.其中一条评论是一个非常好的问题: Optional 的使用会导致性能下降吗? 答案是: 是的,它会的.但是你应该担心吗? 使用Optional的 ...
- Linux + .net core 开发升讯威在线客服系统:首个经过实际验证的高性能版本
业余时间用 .net core 写了一个在线客服系统.并在博客园写了一个系列的文章,写介绍这个开发过程: .net core 和 WPF 开发升讯威在线客服系统:目录 https://blog.she ...
- javascript里面的document.getElementById
一.getElementById:获取对 ID 标签属性为指定值的第一个对象的引用,它有 value 和 length 等属性 1.获取当前页面的值input标签值:var attr1=documen ...
- git分支merge冲突 error: you need to resolve your current index first
问题: 执行切换代码分支 git checkout featrue_2019-06-24 ,报错如下: error: you need to resolve your current index fi ...
- appium自动化测试(5)-一些pyhon操作
1.套件的问题 将所有的测试用例加进去,会一个个执行,用于用例名字没有规范test开头的时候 def suite(): suite = unittest.TestSuite suite.addTest ...
- 字节跳动五面都过了,结果被刷了,问了hr原因竟说是...
说在前面,面试时最好不要虚报工资.本来字节跳动是很想去的,几轮面试也通过了,最后没offer,自己只想到几个原因:1.虚报工资,比实际高30%:2.有更好的人选,这个可能性不大,我看还在招聘.我是面试 ...
- TestNG注释@BeforeGroups与@AfterGroups不执行的处理
在学习TestNG框架注解时发现在执行以下的代码 package com.groups; import org.testng.annotations.AfterGroups; import org.t ...
- 响应式编程基础教程:Spring Boot 与 Lettuce 整合
本文主要介绍响应式编程访问 Redis,以及 Spring Boot 与 Lettuce 的整合使用. Lettuce 是可扩展性线程安全的 Redis 客户端,用于同步.异步和响应式使用.如果多个线 ...
- 计算机毕业设计项目-基于SSM的学生会管理系统-基于ssm的社团信息管理系统
注意:该项目只展示部分功能,如需了解,评论区咨询即可. 1.开发环境 开发语言:Java 后台框架:SSM 前端技术:HTML+CSS+JavaScript+Bootstrap+jQuery 数据库: ...
- pikachu Files Inclusion
文件包含分为远程文件包含和远程文件包含 比如程序员为了提高效率让代码看起来简洁,会使用包含函数的功能,写多个文件 之后需要了进行调用,比如.c写了很多个函数分别在不同的文件里,用的时候直接 引用文件即 ...