这是我参与「第四届青训营」笔记创作活动的第 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
。
他们两个有什么区别?
launchWhenXXX
会在 lifecycleOwner
进入 X
状态之前一直等待,又在离开 X
状态时挂起协程。对应协程只会在生命周期所有者被销毁才会被取消。
这样会造成一定的资源浪费,虽然你成功挂起,但是上流的数据流会不断产生数据。比如你用 StateFlow 去加载实时性很高的点赞数量,如果你返回后台,他还会在后台加载数据,只不过没改动UI而已。这样虽然不会让程序崩溃,但浪费了流量,还损失了一部分手机性能,所以其实这个函数做的并不合适。这时候repeatOnLifecycle
出来了。
repeatOnLifecycle
特点:在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。直接给你把协程停了,不挂起了,从根源上解决问题。
那我为什么使用了whenStarted
而不是更先进的repeatOnLifecycle
呢?是因为加载主页是一个一次性操作,不存在较高实时性。如果在加载时退到后台,协程只是挂起而不是取消,这样用户返回时就能获取到后台加载的数据,而不会再次把协程加载一遍。虽然官方推荐repeatOnLifecycle
,但是launchWhenXXX
和whenXXX
依旧有它发挥的地方。(我的建议是,最好要关闭旋转屏幕后Activity重建,自己管理转屏后的界面能使用户体验感更佳)
咱们刚才看到,StateFlow 其实就是封装过的 SharedFlow,StateFlow 让 SharedFlow 有更强的实时性,成为更强的LiveData。
总结
StateFlow 解决了很多 LiveData 的痛点,值得 Android 开发者进行学习。