一、LiveData的不足
LiveData
的历史要追溯到2017年。彼时,观察者模式有效简化了开发,但诸如 RxJava
一类的库对新手而言有些太过复杂。为此,架构组件团队打造了LiveData
。
LiveData
是一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类,其使用非常简单。
虽然LiveData
结构简单,使用门槛低,但简单也意味着可能不够强大。
LiveData
主要缺点是:
-
LiveData
只能在主线程更新数据,且异步线程修改数据可能存在数据丢失的问题虽然
LiveData
可以在子线程通过postValue
去发布数据,但其实其内部会抛到主线程去执行更新数据,且短时间通过多次postValue
,可能会有丢失数据的问题 -
LiveData
的操作符不够强大,无法应对一些复杂场景
针对LiveData
的上述缺点,Google
推荐使用Flow
来替换LiveData
。
LiveData
整体学习成本更低,且在一些简单场景完全可以满足我们的需求,Google也明确声明不会废弃LiveData
,因为Flow
是基于Kotlin
协程的,如果是用Java
进行Android
开发,则没有办法使用Flow
。
Jetpack Compose
后面应该会成为Android UI开发的主流框架,与Flow
配合更能发挥Kotlin
数据流响应式模型的潜力。基于安全等各方面考虑,学习LiveData
如何迁移到Flow
则有备无患。
二、StateFlow
StateFlow
是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。在Android中,其非常适合需要让可变状态保持可观察的类。
2.1 StateFlow使用
public interface StateFlow<out T> : SharedFlow<T> {
public val value: T
}
public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
public override var value: T
public fun compareAndSet(expect: T, update: T): Boolean
}
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)
StateFlow
是SharedFlow
的子类,其利用同名工厂函数进行创建,且必须设置默认初始值。
简单演示StateFlow
的使用
fun main() {
runBlocking {
val stateFlow = MutableStateFlow(1)
val readOnlyStateFlow = stateFlow.asStateFlow() //只读flow
//模拟外部立即订阅数据
val launch1 = launch(Dispatchers.IO) {
readOnlyStateFlow.collect {
println("模拟外部立即订阅数据 collect $it")
}
}
delay(50)
//模拟在另一个类发送数据
val launch4 = launch(Dispatchers.IO) {
for (i in 1..3) {
println("模拟在另一个类发送数据 wait emit $i")
stateFlow.emit(i)
delay(50)
}
}
//模拟启动页面,在新页面订阅
val launch2 = launch(Dispatchers.IO) {
readOnlyStateFlow.collect{
println("模拟启动页面,在新页面订阅 collectAAA $it")
}
}
val launch3 = launch(Dispatchers.IO) {
readOnlyStateFlow.collect{
println("模拟启动页面,在新页面订阅 collectBBB $it")
}
}
println("get value : ${readOnlyStateFlow.value}")
delay(200)
launch1.cancel()
launch2.cancel()
launch3.cancel()
launch4.cancel()
}
}
/******打印结果******/
模拟外部立即订阅数据 collect 1
模拟在另一个类发送数据 wait emit 1
get value : 1
模拟启动页面,在新页面订阅 collectAAA 1
模拟启动页面,在新页面订阅 collectBBB 1
模拟在另一个类发送数据 wait emit 2
模拟启动页面,在新页面订阅 collectBBB 2
模拟启动页面,在新页面订阅 collectAAA 2
模拟外部立即订阅数据 collect 2
模拟在另一个类发送数据 wait emit 3
模拟外部立即订阅数据 collect 3
模拟启动页面,在新页面订阅 collectAAA 3
模拟启动页面,在新页面订阅 collectBBB 3
StateFlow
没有发送数据时订阅,会先接收默认值。- 如果要屏蔽外部发送污染数据,只对外部提供只读属性的
StateFlow
,可用asStateFlow
进行转化 - 新发送的数据,如果第一个值与默认值相同,则直接被过滤了
- 后续新添加的订阅者能够接收到的只有最新值
再看一下官方提供的示例
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
sealed class LatestNewsUiState {
data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(exception: Throwable): LatestNewsUiState()
}
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
latestNewsViewModel.uiState.collect { uiState ->
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
警告:如需更新界面,切勿使用
launch
或launchIn
扩展函数从界面直接收集数据流。即使View不可见,这些函数也会处理事件,可能造成应用崩溃。避免上述情况,使用repeatOnLifecycle
API
StateFlow
与LiveData
至此可发现StateFlow
与LiveData
具有很多相似之处,单再某些行为存在差异:
StateFlow
创建时需要默认初始值,LiveData
则不需要View
进入Stop
状态,LiveData.observe()
会自动取消注册使用方,而StateFlow
或其他流收集数据的操作不会自动停止。若需实现相同行为,可在Lifecycle.repeatOnLifecycle
块收集数据流。
2.2 数据流收集
View
进入Stop
状态,StateFlow
与其他流收集数据的操作是不会自动停止,看一下如何使用更安全的方式收集数据流。
要收集数据流,需要用到协程。Activity
与Fragment
提供了若干个协程构建器
Activity.lifecycleScope.launch
:立即启动协程,在本Activity
销毁时结束协程Fragment.lifecycleScope.launch
:立即启动协程,在本Fragment
销毁时结束协程Fragment.viewLifecycleOwner.lifecycleScope.launch
:立即启动协程,在本Fragment
中的视图生命周期结束时取消协程
除此之外,对于一个状态X,有专门的launch
方法称为launchWhenX
。会在 lifecycleOwner
进入 X
状态之前一直等待,又在离开 X
状态时挂起协程。对应协程只会在生命周期所有者被销毁才会被取消。
当应用在后台运行时接收数据更新可能会引起应用崩溃,这种情况可通过将视图的数据流收集操作挂起来解决,但是上游数据流会在应用后台运行期间保持活跃,浪费一定的资源。
repeatOnLifecycle
协程构建器
该协程构建器(lifecycle 2.4.0-alpha01
版本引入)特点:在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 单次配置任务
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
// 对 expensiveObject 进行操作
}
// 当协程恢复时,`lifecycle` 处于 DESTROY 状态。repeatOnLifecycle 会在
// 进入 DESTROYED 状态前挂起协程的执行
}
}
}
上述API需要在 activity
的 onCreate
或者 fragment
的 onViewCreated
方法中去执行. 这样可以避免产生位置的异常行为。
Fragment
应该始终使用 viewLifecycleOwner
去触发 UI
更新,但是这不适用于 DialogFragment
,因为它可能没有 View
.。对于 DialogFragment
,应该使用 lifecycleOwner
若只需收集一个数据流,可以使用 Flow.flowWithLifecycle
操作符,其内部也是使用 suspend Lifecycle.repeatOnLifecycle
函数实现
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地图
}
.launchIn(lifecycleScope)
}
}
2.3 stateIn
stateIn
能够将普通的流转换为StateFlow
,其必须要设置默认值,且转换的共享数据流只缓存一个最新值。
public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T> {
val config = configureSharing(1) //配置共享流只缓存一个值
val result = CompletableDeferred<StateFlow<T>>() //创建新实例
scope.launchSharingDeferred(config.context, config.upstream, result)
return result.await()
}
public fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T> {
val config = configureSharing(1)
val state = MutableStateFlow(initialValue)
val job = scope.launchSharing(config.context, config.upstream, state, started, initialValue)
return ReadonlyStateFlow(state, job)
}
参数 | 含义 |
---|---|
scope | 共享开始时所在的协程作用域范围 |
started | 控制共享的开始和结束的策略 |
initialValue | 状态流初始值 |
其中started
接受三个值:
Lazily
:当首个订阅者出现时开始,后续消费者只能收到历史缓存与后续数据。在scope
指定的作用域被结束时终止。Eagerly
:立即开始发送数据,后续消费者只能收到历史缓存与新数据。在scope
指定的作用域被结束时终止。WhileSubscribed
:等待第一个消费者订阅后,才开始发送数据源。可配置在最后一个订阅者关闭后,共享数据流上游停止时间与历史缓存清空时间
fun main() {
runBlocking {
val flow= flow {
List(10) {
emit(it)
}
}.stateIn(this)
launch(Dispatchers.IO) {
flow.collect {
println("result $it")
}
}
}
Thread.sleep(2000)
}
/*******打印结果******/
result 9
三、SharedFlow
SharedFlow
是热数据流,只要该数据流被收集,或对它的任何其他引用在垃圾回收根中存在,该数据流就会一直存于内存中。
3.1 SharedFlow使用
public interface SharedFlow<out T> : Flow<T> {
public val replayCache: List<T>
}
SharedFlow
本身的定义仅比Flow
多了历史数据缓存的集合,只允许订阅数据
SharedFlow
存在一个可变版本MutableSharedFlow
,定义了发送数据的功能
public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
//线程安全的挂起函数发送数据
override suspend fun emit(value: T)
//线程安全的尝试发送数据
public fun tryEmit(value: T): Boolean
//共享数据流的订阅者数量
public val subscriptionCount: StateFlow<Int>
//重置历史数据缓存
public fun resetReplayCache()
}
看一下SharedFlow
的构造函数
public fun <T> MutableSharedFlow(
replay: Int = 0, //缓存的历史数据容量
extraBufferCapacity: Int = 0, //除历史数据外的额外缓冲区容量
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND //被压策略
): MutableSharedFlow<T> {
......
val bufferCapacity0 = replay + extraBufferCapacity
val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}
参数 | 含义 |
---|---|
reply | 历史元素缓存区容量;历史缓冲区满,则移除最早的元素。新消费者订阅了该数据流,先将历史缓存区元素依次发送给新的消费者,然后才发送新元素。 |
extraBufferCapacity | 除历史缓存区外的额外缓存区容量,用于扩充内部整体缓存容量 |
onBufferOverflow | 缓存区背压策略;默认是BufferOverflow.SUSPEND ,当额外缓冲区满后,挂起emit 函数,暂停发送数据。只有在replay 和extraBufferCapacity 均不为0时才支持其他背压策略。 |
SharedFlow
简单使用演示
fun main() {
testFlow()
}
fun testFlow() = runBlocking {
val sharedFlow = MutableSharedFlow<String>(replay = 1)
launch(Dispatchers.IO) {
for (i in 0..5) {
sharedFlow.emit("data$i")
delay(50)
}
}
//模拟外部调用
delay(110)
val readOnlySharedFlow = sharedFlow.asSharedFlow()
launch(Dispatchers.IO) {
readOnlySharedFlow.map {
"$it receiver AAA"
}.collect {
println(it)
}
}
delay(50)
launch(Dispatchers.IO) {
readOnlySharedFlow.map {
"$it receiver BBB"
}.collect {
println(it)
}
}
}
/******打印结果******/
data2 receiver AAA
data3 receiver AAA
data3 receiver BBB
data4 receiver BBB
data4 receiver AAA
data5 receiver AAA
data5 receiver BBB
- 即使没有设置消费者订阅,依然执行数据发送操作。前述示例未设置历史缓存,故data1等部分数据被抛弃了
- 若只允许外部进行数据流订阅,可调用
asSharedFlow
函数,转化为只读的SharedFlow
- 对于同一个数据流,可以允许有多个订阅者共享
- 前述
SharedFlow
创建时设置replay
属性,例如设置为2,就可缓存最新的两个值 SharedFlow
内部是基于数组 +synchronized
,是线程安全的。
再看一下官方的示例
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
val tickFlow: SharedFlow<Event<String>> = _tickFlow
init {
externalScope.launch {
while(true) {
_tickFlow.emit(Unit)
delay(tickIntervalMs)
}
}
}
}
class NewsRepository(
...,
private val tickHandler: TickHandler,
private val externalScope: CoroutineScope
) {
init {
externalScope.launch {
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}
3.2 shareIn
普通的flow
可以使用shareIn
扩展方法,转化成SharedFlow
public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T> {
//配置共享流
val config = configureSharing(replay)
val shared = MutableSharedFlow<T>(
replay = replay,
extraBufferCapacity = config.extraBufferCapacity,
onBufferOverflow = config.onBufferOverflow
)
//在给定的协程作用域以给定配置启动协程
@Suppress("UNCHECKED_CAST")
val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
return ReadonlySharedFlow(shared, job)
}
其中started
表示新创建的共享数据流的启动与停止策略,接受三个值:
-
Lazily
:当首个订阅者出现时开始,后续消费者只能收到历史缓存与后续数据。在scope
指定的作用域被结束时终止。 -
Eagerly
:立即开始发送数据,后续消费者只能收到历史缓存与新数据。在scope
指定的作用域被结束时终止。 -
WhileSubscribed
:等待第一个消费者订阅后,才开始发送数据源。可配置在最后一个订阅者关闭后,共享数据流上游停止时间(默认立即停止)与历史缓存清空时间(默认永远保留)public fun WhileSubscribed( stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE )
shareIn
可将消耗一次资源从数据源获取数据的Flow
数据流,转化为SharedFlow
,实现一对多的事件分发,减少多次调用资源的损耗。
使用shareIn
每次会创建一个新的SharedFlow
实例,且该实例会一直保留在内存中,直到垃圾回收。故最好减少转换流执行次数。
四、stateIn与shareIn使用须知
stateIn
与shareIn
操作符将冷流转换为热流:可以将来自上游冷数据流的信息广播给多个收集者。
这两个操作符通常用于性能提升;没有收集者时加入缓冲;作为一种缓存机制使用。更多内容参看官方文档:Flow操作符 shareIn和 stateIn使用须知
五、LiveData迁移
Android
官方指导文档从LiveData迁移到Kotlin数据流其实已经讲的较为详细了,这里仅在原文的基础上进行补充。
下面针对LiveData
的常见场景,比较二者之间的实现差异
使用可变数据存储器暴露一次性操作的结果
看一下LiveData
的实现方式
class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
在Kotlin
数据流中,需要使用(可变的) StateFlow
(状态容器式可观察数据流)实现
class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState
// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}
注意:
StateFlow
始终是有值的且值是唯一的,创建时需要赋予初始值StateFlow
允许被多个观察者共用StateFlow
永远只会把最新的值重现给订阅者,与活跃观察者数量无关- 如果
StateFlow
重复赋予同一个值,只会回调一次
把一次性操作的结果暴露出来
效果与前述一致,只是这里暴露协程调用的结果而无需使用可变属性
对于LiveData
,需要使用LiveData
协程构建器
class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}
对于StateFlow
class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), //由于是一次性操作,也可以使用 Lazily
initialValue = Result.Loading
)
}
这里使用stateIn
操作符将数据流转换为StateFlow
带参数的一次性数据加载
例如要加载一些依赖用户ID的数据,而信息来自一个提供数据流的AuthManager
对于LiveData
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
liveData { emit(repository.fetchItem(newUserId)) }
}
}
亦或者将流式数据与Flow
结合
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.asLiveData()
}
对于StateFlow
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }
val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}
亦或者
val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser //注意此处不同的加载状态
)
观察带参数的数据流
对于LiveData
,可以将数据流转换为 LiveData
实例,然后通过 emitSource
传递数据的变化
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()
val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}
亦或者
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.asLiveData()
}
对于StateFlow
class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }
val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser
)
}
5.1 stateIn配置
val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser
)
关于stateIn
运算符中的参数含义见前述内容,这里重点说下WhileSubscribed
WhileSubscribed
策略会在没有收集器的情况下取消上游数据流。
通过 stateIn
运算符创建的 StateFlow
会把数据暴露给视图 (View
),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当应用在后台运行时,应当保持克制并中止这些协程。
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)
超时停止:stopTimeoutMillis
,以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止)
如果不想因为视图有几秒钟不再监听就结束上游流(例如用户旋转设备,视图销毁重建),可以设置该值。前述LiveData
构建器使用的方法是添加一个5秒钟延迟,即如果等待 5 秒后仍然没有订阅者存在就终止协程。
应用场景体现:
- 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
- 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
- 订阅将被重启,新数据会填充进来,当数据可用时更新视图。
数据重现的过期日期
如果用户离开应用太久,此时您不想让用户看到陈旧的数据,并且希望显示数据正在加载中,那么就应该在 WhileSubscribed
策略中使用 replayExpirationMillis
参数。这种情况下此参数非常适合,由于缓存的数据都恢复成了 stateIn
中定义的初始值,因此可以有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过期的信息显示出来
replayExpirationMillis
配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn
运算符中定义的初始值 initialValue
) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE
(表示永远不将其重置)。如果设置为 0
,可以在符合条件时立即重置缓存的数据。
综上所述,通过ViewModel
暴露数据,并在视图中获取的最佳方式是:
- 使用带超时参数的
WhileSubscribed
策略暴露StateFlow
- 使用
repeatOnLifecycle
来收集数据更新