LiveData迁移到StateFlow

3,784 阅读10分钟

一、LiveData的不足

LiveData的历史要追溯到2017年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了LiveData

LiveData是一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类,其使用非常简单。

虽然LiveData结构简单,使用门槛低,但简单也意味着可能不够强大。

LiveData主要缺点是:

  1. LiveData只能在主线程更新数据,且异步线程修改数据可能存在数据丢失的问题

    虽然LiveData可以在子线程通过postValue去发布数据,但其实其内部会抛到主线程去执行更新数据,且短时间通过多次postValue,可能会有丢失数据的问题

  2. 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)

StateFlowSharedFlow的子类,其利用同名工厂函数进行创建,且必须设置默认初始值。

简单演示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
  1. StateFlow没有发送数据时订阅,会先接收默认值。
  2. 如果要屏蔽外部发送污染数据,只对外部提供只读属性的StateFlow,可用asStateFlow进行转化
  3. 新发送的数据,如果第一个值与默认值相同,则直接被过滤了
  4. 后续新添加的订阅者能够接收到的只有最新值

再看一下官方提供的示例

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)
                    }
                }
            }
        }
    }
}

警告:如需更新界面,切勿使用launchlaunchIn扩展函数从界面直接收集数据流。即使View不可见,这些函数也会处理事件,可能造成应用崩溃。避免上述情况,使用repeatOnLifecycle API

StateFlowLiveData

至此可发现StateFlowLiveData具有很多相似之处,单再某些行为存在差异:

  1. StateFlow创建时需要默认初始值,LiveData则不需要
  2. View进入Stop状态,LiveData.observe() 会自动取消注册使用方,而 StateFlow 或其他流收集数据的操作不会自动停止。若需实现相同行为,可在Lifecycle.repeatOnLifecycle 块收集数据流。

2.2 数据流收集

View进入Stop状态,StateFlow与其他流收集数据的操作是不会自动停止,看一下如何使用更安全的方式收集数据流。

要收集数据流,需要用到协程。ActivityFragment提供了若干个协程构建器

  • 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需要在 activityonCreate 或者 fragmentonViewCreated 方法中去执行. 这样可以避免产生位置的异常行为。

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接受三个值:

  1. Lazily:当首个订阅者出现时开始,后续消费者只能收到历史缓存与后续数据。在 scope 指定的作用域被结束时终止。
  2. Eagerly:立即开始发送数据,后续消费者只能收到历史缓存与新数据。在 scope 指定的作用域被结束时终止。
  3. 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函数,暂停发送数据。只有在replayextraBufferCapacity均不为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
  1. 即使没有设置消费者订阅,依然执行数据发送操作。前述示例未设置历史缓存,故data1等部分数据被抛弃了
  2. 若只允许外部进行数据流订阅,可调用asSharedFlow函数,转化为只读的SharedFlow
  3. 对于同一个数据流,可以允许有多个订阅者共享
  4. 前述SharedFlow创建时设置replay属性,例如设置为2,就可缓存最新的两个值
  5. 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表示新创建的共享数据流的启动与停止策略,接受三个值:

  1. Lazily:当首个订阅者出现时开始,后续消费者只能收到历史缓存与后续数据。在 scope 指定的作用域被结束时终止。

  2. Eagerly:立即开始发送数据,后续消费者只能收到历史缓存与新数据。在 scope 指定的作用域被结束时终止。

  3. WhileSubscribed:等待第一个消费者订阅后,才开始发送数据源。可配置在最后一个订阅者关闭后,共享数据流上游停止时间(默认立即停止)与历史缓存清空时间(默认永远保留)

    public fun WhileSubscribed(
       stopTimeoutMillis: Long = 0,
       replayExpirationMillis: Long = Long.MAX_VALUE
    )
    

shareIn可将消耗一次资源从数据源获取数据的Flow数据流,转化为SharedFlow,实现一对多的事件分发,减少多次调用资源的损耗。

使用shareIn每次会创建一个新的SharedFlow实例,且该实例会一直保留在内存中,直到垃圾回收。故最好减少转换流执行次数。

四、stateIn与shareIn使用须知

stateInshareIn操作符将冷流转换为热流:可以将来自上游冷数据流的信息广播给多个收集者。

这两个操作符通常用于性能提升;没有收集者时加入缓冲;作为一种缓存机制使用。更多内容参看官方文档: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
        }
    }
}

注意:

  1. StateFlow始终是有值的且值是唯一的,创建时需要赋予初始值
  2. StateFlow允许被多个观察者共用
  3. StateFlow永远只会把最新的值重现给订阅者,与活跃观察者数量无关
  4. 如果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 秒后仍然没有订阅者存在就终止协程。

应用场景体现:

  1. 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
  2. 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
  3. 订阅将被重启,新数据会填充进来,当数据可用时更新视图。

数据重现的过期日期

如果用户离开应用太久,此时您不想让用户看到陈旧的数据,并且希望显示数据正在加载中,那么就应该在 WhileSubscribed 策略中使用 replayExpirationMillis 参数。这种情况下此参数非常适合,由于缓存的数据都恢复成了 stateIn 中定义的初始值,因此可以有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过期的信息显示出来

replayExpirationMillis 配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。如果设置为 0,可以在符合条件时立即重置缓存的数据。

综上所述,通过ViewModel 暴露数据,并在视图中获取的最佳方式是:

  1. 使用带超时参数的WhileSubscribed 策略暴露 StateFlow
  2. 使用 repeatOnLifecycle 来收集数据更新