这是我参与「第四届青训营」笔记创作活动的第 5 天。
在这次组队大项目中,我初次了解到了 Kotlin 这门新兴语言。为了能使本项目顺利开展,我也对这门语言进行了一定的学习,也对 Android 有更深刻的认识。接下来我将会写一些我对 Android 的理解。
SharedFlow
简介
public interface SharedFlow<out T> : Flow<T> {
/**
* A snapshot of the replay cache.
*/
public val replayCache: List<T>
override suspend fun collect(collector: FlowCollector<T>): Nothing
}
SharedFlow 本身的定义仅比 Flow 多了历史数据缓存的集合,只允许订阅数据。StateFlow 能干的,SharedFlow 都能干。它也是人如其名,共享流。不管是 Activity 还是 Fragment,只要他们相关联(比如共享一个 ViewModel),并且都 collect 一个相同的 Flow,只要在一个地方给 Flow 赋值,所有 collect 全部响应。
注意:SharedFlow 默认是等到订阅者全部接收到并处理完成之后,才会进行下一次发送,否则就会挂起。
StateFlow 一般用于处理粘性事件问题,比如点赞数量,而 SharedFlow 一般用于处理一些非粘性事件的问题,比如成功弹窗。
StateFlow、SharedFlow、LiveData 有共同特点,都含一个Mutable,一个Immutable。
你去看一下 MutableSharedFlow 的参数,似乎比 MutableStateFlow 要复杂了一点:
// MutableStateFlow
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)
// MutableSharedFlow
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
由此可见,SharedFlow 是没有默认值的,但这些参数又是些什么玩意?
-
replay :重播给新订阅者的值的数量,默认为零,不能为负。
-
extraBufferCapacity :除重播外缓冲的值的数量,当有剩余缓冲区空间时,emit 不会挂起,默认为零,不能为负。
-
onBufferOverflow :配置缓冲区溢出的操作。他接收 BufferOverflow 的参数
public enum class BufferOverflow { /** * Suspend on buffer overflow. */ SUSPEND, /** * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. */ DROP_OLDEST, /** * Drop **the latest** value that is being added to the buffer right now on buffer overflow * (so that buffer contents stay the same), do not suspend. */ DROP_LATEST }
BufferOverflow
是一个枚举,它有三个量:SUSPEND
、DROP_OLDEST
、DROP_LATEST
。默认为
SUSPEND
,也就是缓存溢出时挂起。其余两个也如其名,
DROP_OLDEST
是溢出时删除缓冲区中最旧的值。将新值添加到缓存区,不挂起;DROP_LATEST
,在缓冲区溢出时删除当前添加到缓冲区的最新值,以便缓冲区内容保持不变,不挂起。
我说这么多估计也不好理解,这几个参数简而言之就是为了让你可以把 SharedFlow 改造成一个可以自定义的粘性事件。你可以私下里从你的项目里创建一个 SharedFlow,简单地把 replay 设置成 3,自行探究一下粘性事件的发生。
注意几点,它没有初始值,没有防抖(但可以防抖)。
使用
假设一个场景,我们需要对一个视频进行收藏,收藏之后无论成功失败都要弹出 Toast 提醒。
在之前使用 LiveData 实现这个功能的时候,总会遇到粘性事件的问题。我触发该事件之后显示了 Toast,结果我一旋转屏幕,Toast 又出来了。我们肯定不想要这种情况。这时候可以用 StateFlow 来代替。
首先把 Retrofit 配置好,我随便找了一段作为例子:
@FormUrlEncoded
@POST("like")
suspend fun addToMyFavVideo(
@Field("like-foreign-id") videoCode: String,
@Field("like-status") likeStatus: String,
@Header("X-CSRF-TOKEN") csrfToken_1: String?,
@Field("_token") csrfToken_2: String?,
@Field("like-user-id") userId: String?,
@Field("like-is-positive") likeIsPositive: Int = 1
): AddFavVideoModel
然后去仓库层进行封装,大概样子跟这个差不多,这里跟 StateFlow 在仓库层的设置差不多:
object NetworkRepo {
fun addToMyFavVideo(...) = flow {
try {
emit(WebsiteState.Loading())
val successResult = Network.service.addToMyFavVideo(...)
emit(WebsiteState.Success(successResult))
} catch (e: Exception) {
emit(WebsiteState.Error(e))
}
}.flowOn(Dispatchers.IO) // 子线程执行
}
去 ViewModel 中进行 SharedFlow 的配置,发现和 StateFlow 也差不多。不过 StateFlow 在 collect 时使用的是 value,而 SharedFlow 在 collect 时使用的是 emit:
private val _addToFavVideoFlow = MutableSharedFlow<WebsiteState<AddFavVideoModel>>()
val addToFavVideoFlow = _addToFavVideoFlow.asSharedFlow()
// 该事件一般用于点击,所以无需 init 初始化
fun addToFavVideo(...) {
viewModelScope.launch {
NetworkRepo.addToMyFavVideo(...)
.collect {
_addToFavVideoFlow.emit(it)
}
}
}
然后到 Fragment 里进行 Flow 收集:
@OnClick(...)
viewModel.addToFavVideo(...)
viewLifecycleOwner.lifecycleScope.launch {
whenStarted {
viewModel.addToFavVideoFlow.collect { state ->
when (state) {
is WebsiteState.Error -> {
showShortToast("收藏失敗")
}
is WebsiteState.Loading -> {
}
is WebsiteState.Success -> {
showShortToast("收藏成功")
}
}
}
}
}
这样就ok了,SharedFlow 默认就是一次性事件,如果你不去改动它的参数,它就相当于 SingleLiveData。
你点击按钮后,进行加入收藏操作,成功之后会弹出 收藏成功 的 Toast,失败会弹出 收藏失败 的 Toast。如果旋转屏幕,Toast 不会二次生成。如果想让他成为粘性事件,可以试着修改一下 MutableSharedFlow 的参数。
总结
SharedFlow 适用于事件消费。旧的都可以要也可以不要,有时更希望每条事件都执行。