Android 基础之 SharedFlow | 青训营笔记

521 阅读4分钟

这是我参与「第四届青训营」笔记创作活动的第 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是一个枚举,它有三个量:SUSPENDDROP_OLDESTDROP_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 适用于事件消费。旧的都可以要也可以不要,有时更希望每条事件都执行。