Android 基础之 StateFlow | 青训营笔记

391 阅读5分钟

这是我参与「第四届青训营」笔记创作活动的第 4 天。

在这次组队大项目中,我初次了解到了 Kotlin 这门新兴语言。为了能使本项目顺利开展,我也对这门语言进行了一定的学习,也对 Android 有更深刻的认识。接下来我将会写一些我对 Android 的理解。

StateFlow

简介

State Flow,人如其名,注重于State(状态)。状态流,表明了它需要展现状态,状态可能会是不断变化的,所以它做成了和 LiveData 类似的效果,但比 LiveData 更强大。

使用

假设一个场景,我们在MVVM架构下需要加载一个主页,实时性不高。

加载主页,它有几种状态?很明显至少有三种:加载,失败,成功。

我需要获得这三个状态,第一反应就是整三个常量,或者整个枚举。

object WebsiteState {
    const val Loading = 0
    const val Failure = 1
    const val Success = 2
}
​
enum class WebsiteState {
    LOADING,
    FAILURE,
    SUCCESS;
}

这样确实可行,但是你需要的是既要状态,又要状态带来的结果。我加载页面成功了,但我还需要成功后返回的结果;加载失败了,我需要它具体抛出的异常。这时候,kotlin 的 sealed class 就派上用场了。

我们根据上者构建一个简单的 sealed class:

sealed class WebsiteState<T> {
    data class Success<T>(val info: T) : WebsiteState<T>()
    class Loading<T> : WebsiteState<T>()
    data class Error<T>(val throwable: Throwable) : WebsiteState<T>()
}

sealed class 中有三种状态,其中成功和失败为数据类,而加载作为一种状态,只需要把它作为一种普通类就可以了。泛型能使该密封类传入不同的数据类型。

你用 Retrofit 把主页的东西布置好,

@GET(".")
suspend fun getHomePage(): HomePageModel

然后去仓库层进行封装,大概样子跟这个差不多:

object NetworkRepo {
    fun getHomePage() = flow {
        try {
            emit(WebsiteState.Loading())
            val successResult = Network.service.getHomePage()
            emit(WebsiteState.Success(successResult))
        } catch (e: Exception) {
            emit(WebsiteState.Error(e))
        }
    }.flowOn(Dispatchers.IO) // 子线程执行
}

然后去 HomePageViewModel 进行操作:

private val _homePageFlow =
    MutableStateFlow<WebsiteState<HomePageModel>>(WebsiteState.Loading())
val homePageFlow = _homePageFlow.asStateFlow() // 变成不可变// 方法的 launch 只用在点击等延迟事件中
fun getHomePage() {
    viewModelScope.launch {
        NetworkRepo.getHomePage().collect { homePage ->
            _homePageFlow.value = homePage
        }
    }
}
​
// 整个 ViewModel 中就初始化加载这一次,
// 想重新加载去调用上面的方法
init {
    viewModelScope.launch {
        NetworkRepo.getHomePage().collect { homePage ->
            _homePageFlow.value = homePage
        }
    }
}

你看这个,是不是跟你之前写的 LiveData 很像?咱来对比一下LiveData:

private val _homePageLiveData = MutableLiveData<WebsiteState<HomePageModel>>()
val homePageLiveData = _homePageLiveData.switchMap {
    NetworkRepo.getHomePage()
}
​
fun getHomePage() {
    _homePageLiveData.value = WebsiteState.Loading()
    // 或者等于自身
    // _homePageLiveData.value = _homePageLiveData.value
    // 因为获取主页信息不需要任何参数
}
​
init {
    ...
}

发现大差不差,都是一个可变数据流,一个不可变数据流,一个函数负责操作可变数据流。

但是,StateFlow 要比 LiveData 多了点东西,在 MutableStateFlow 中,必须传一个东西——初始值,这就和 LiveData 不大一样。

public interface StateFlow<out T> : SharedFlow<T> {
    /**
     * The current value of this state flow.
     */
    public val value: T
}

ViewModel 配置好了,接下来转向 Activity:

override fun onCreate(...) {
    super.onCreate(...)
    binding.homePageSrl.apply {
        setOnRefreshListener {
            // will enter here firstly. cuz the flow's def value is Loading.
            viewModel.getHomePage()
        }
        setEnableLoadMore(false)
    }
    lifecycleScope.launch {
        whenStarted {
            viewModel.homePageFlow.collect { state ->
                binding.homePageNsv.isGone = state !is WebsiteState.Success
                binding.errorTip.isVisible = state is WebsiteState.Error
                when (state) {
                    is WebsiteState.Loading -> {
                        // SmartRefreshLayout 的特性,使用该函数会直接触发listener
                        binding.homePageSrl.autoRefresh()
                    }
                    is WebsiteState.Success -> {
                        binding.homePageSrl.finishRefresh()
                        latestHanimeAdapter.setList(state.info.latestHanime)
                        latestUploadAdapter.setList(state.info.latestUpload)                 
                        hanimeCurrentAdapter.setList(state.info.hanimeCurrent)
                        hanimeTheyWatchedAdapter.setList(state.info.hanimeTheyWatched)
                    }
                    is WebsiteState.Error -> {
                        binding.homePageSrl.finishRefresh()
                        binding.errorTip.text = "🥺\n${state.throwable.message}"
                    }
                }
            }
        }
    }
}

因为StateFlow要求有初始值,所以第一次collect会进入Loading,Loading触发Listener,进行ViewModel中的方法getHomePage(),这个方法就做了一件事情——在ViewModel的作用域中,执行仓库中的getHomePage(),网络加载后把结果给可变的_homePageFlow赋值。最后Activity中viewModel.homePageFlow收到_homePageFlow的值,并且按照我们的需求进行下一步UI操作。一套下来,非常的优雅,再配套上密封类的加成,维护起来会非常的方便。

这里说一下,LiveData自身不支持防抖而StateFlow自身支持防抖。什么是防抖?防抖就是如果刷新后和刷新前的数据是一样的,数据就不变化。我们有时候经常看到一些界面一刷新就会闪一下,这就是没做防抖的问题。LiveData自身不支持不代表不能防抖,给LiveData加上distinctUntilChanged()就可以了。

附StateFlow的防抖部分源码:

if (oldState == newState) return true // Don't do anything if value is not changing, but CAS -> true

与LiveData不同,Flow收集不自带生命周期,需要自己配置,以 fragment 中 collect 为例:

// LiveData
homePageLiveData.observe(viewLifecycleOwner) {
    ...
}
​
// Flow
viewLifecycleOwner.lifecycleScope.launch {
    homePageFlow.flowWithLifecycle(viewLifecycleOwner.lifecycle).collect {
        ...
    }
}

Activity 直接用 lifecycleScope,Fragment 用 viewLifecycleOwner.lifecycleScope 这很好理解,但是你看,launch 之后,一会又whenStarted,一会又flowWithLifecycle,一会又有什么launchWhenStarted,过会又冒出来个repeatOnLifecycle,到底该用哪个?

public fun <T> Flow<T>.flowWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowWithLifecycle.collect {
            send(it)
        }
    }
    close()
}

可见flowWithLifecycle作用约等于repeatOnLifecycle

public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
    lifecycle.whenStarted(block)
}

可见launchWhenStarted作用等于 launch 后的whenStarted

他们两个有什么区别?

differences

launchWhenXXX会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。对应协程只会在生命周期所有者被销毁才会被取消。

这样会造成一定的资源浪费,虽然你成功挂起,但是上流的数据流会不断产生数据。比如你用 StateFlow 去加载实时性很高的点赞数量,如果你返回后台,他还会在后台加载数据,只不过没改动UI而已。这样虽然不会让程序崩溃,但浪费了流量,还损失了一部分手机性能,所以其实这个函数做的并不合适。这时候repeatOnLifecycle出来了。

repeatOnLifecycle特点:在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。直接给你把协程停了,不挂起了,从根源上解决问题。

那我为什么使用了whenStarted而不是更先进的repeatOnLifecycle呢?是因为加载主页是一个一次性操作,不存在较高实时性。如果在加载时退到后台,协程只是挂起而不是取消,这样用户返回时就能获取到后台加载的数据,而不会再次把协程加载一遍。虽然官方推荐repeatOnLifecycle,但是launchWhenXXXwhenXXX依旧有它发挥的地方。(我的建议是,最好要关闭旋转屏幕后Activity重建,自己管理转屏后的界面能使用户体验感更佳)

咱们刚才看到,StateFlow 其实就是封装过的 SharedFlow,StateFlow 让 SharedFlow 有更强的实时性,成为更强的LiveData。

总结

StateFlow 解决了很多 LiveData 的痛点,值得 Android 开发者进行学习。