ShareFlow与StateFlow实战

前言

本文主要基于具体场景介绍StateFlow和ShareFlow的使用

场景实战

首页1s刷新一次

kotlin 复制代码
// onresume

fun startRefreshUi() {

    refreshJob = launch(Dispatchers.Main) {

        while (_isRefreshUiRunning) {

            _todayDataFlow.emitAll(

                getTodayUseCase(

                    GetTodayUseCase.Params(uid, KevaUtil.getBoolean(NEED_REQUEST_KEY, true))

                )

            )

            delay(REFRESH_DATA_DURATION)

        }

    }

}



// onstop

fun stopRefreshUi() {

    Log.d(TAG, "stopRefreshUi")

    _isRefreshUiRunning = false

}
  • onResume开始刷新
  • onStop 停止刷新

优化如下

markdown 复制代码
/**

    * [REFRESH_DATA_DURATION] 刷新一次today

    */

private val refreshTodayFlow = flow {

    while (true) {

        Log.i("yyyyyyyyyy", ": emitAll OnlineOrOffline")

        emit(Unit)

        delay(REFRESH_DATA_DURATION)

    }

}



val todayDataFlow: SharedFlow<Res<TodayData>> = refreshTodayFlow.transform {

    UserBean.getInstance()?.uniqid?.let { uid ->

        emitAll(getTodayUseCase(

            GetTodayUseCase.Params(uid, KevaUtil.getBoolean(NEED_REQUEST_KEY, true))

        ))

    }

}.stateIn(viewModelScope, SharingStarted.WhileSubscribed() , Res.Loading)

SharingStarted.WhileSubscribed()在onStop时 会立即停止上游更新。ShareFlow和StateFlow中的生命周期管理 - 文章 - ByteTech (bytedance.net)

分页处理

场景分析

使用jetpack paging3进行分页, paging3内部做了flow的适配

markdown 复制代码
val pagingRankingData: MutableLiveData<Flow<PagingData<RankingsItem>>?> = MutableLiveData()

private val pagerChangedFlow = combineTransform(

        rakingKeyWrapper,

        rakingDuration,

        updateLocationSignal,

        reloadSignal

    ) { rankingKeyWrapper, duration, updateLocationSignal, reload ->

        Log.i("yyyyy", "1231")

        val rankingKey = rankingKeyWrapper?.data

        nowRankingKey = rankingKey

        nowRankingDuration = duration

        // 1. 之前这样处理的原因是, 如果emitAll(pager.flow),只有第一次能执行,后续都不能进入combineTransform的 {}

        pagingRankingData.value = Pager(PagingConfig(pageSize = pageSize)) {

                    val source = RankingListSource(

                        rankingKey?.key,

                        duration,

                        pageSize

                    ) { rankingData, page ->

                        if (page == RankingListSource.FIRST_PAGE) {

                            //只有加载第一页的时候我们才刷新这个

                            viewModelScope.launch {

                                _rankingExtData.emit(rankingData)

                            }

                        }

                    }

                    return@Pager source

                }.flow             

        emit(emptyFlow<Int>())

    }.stateIn(viewModelScope, SharingStarted.Eagerly, 1) // 2. SharingStarted.Eagerly不太对

问题

简化模型

简化场景模型如下:

kotlin 复制代码
private val pageFlowSignal = MutableSharedFlow<Int>(0)

private val pageFlow = MutableStateFlow(0)



private val testFlow1 = MutableStateFlow(1)

private val testFlow2 = MutableStateFlow("1")

val testFlow = combineTransform(testFlow1, testFlow2, pageFlowSignal) { a, b, _ ->

    Log.i("yyyyyyy", ": combineTransform")

    // emitAll 一个热流

    emitAll(pageFlow)

}

// 每次点击button触发

fun test() {

    viewModelScope.launch {

        pageFlowSignal.emit((0..1100).random())

    }

}

通过点击button按钮触发test函数,但只有第一次会打印yyyyyyy

  • 在combineTransform里不要emitAll(热流)

改造

markdown 复制代码
val pagerChangedFlow = combineTransform(

        rakingKeyWrapper,

        rakingDuration,

        updateLocationSignal,

        reloadSignal

    ) { rankingKeyWrapper, duration, updateLocationSignal, reload ->

        val rankingKey = rankingKeyWrapper?.data

        nowRankingKey = rankingKey

        nowRankingDuration = duration

        val rankingKeyJson = GsonUtils.toJson(rankingKeyWrapper?.data)

        KevaUtil.putString(GetInitedRankingKeyUseCase.SAVED_RANKING_KEY, rankingKeyJson)

        KevaUtil.putString(

            GetInitedRankingDurationUseCase.SAVED_RANKING_DURATION,

            duration.duration

        )

        emit(Pair(rankingKey, duration))

    }.flatMapLatest {

        XLog.tag(TAG).i(" enter page flow ")

        val (rankingKey, duration) = it

        if (rankingKey?.isNeedLocate == true && !fitLocationUtil.hasLocationPermission()) {

            XLog.tag(TAG)

                .d("pagingRankingData needLocate true and don't hasLocationPermission.")

            flow {

                emit(PagingData.empty<RankingsItem>())

            }

        } else {

            val pageSize = 50

            Pager(PagingConfig(pageSize = pageSize)) {

                val source = RankingListSource(

                    rankingKey?.key,

                    duration,

                    pageSize

                ) { rankingData, page ->

                    if (page == RankingListSource.FIRST_PAGE) {

                        //只有加载第一页的时候我们才刷新这个

                        viewModelScope.launch {

                            _rankingExtData.emit(rankingData)

                        }

                    }

                }

                return@Pager source

            }.flow

        }

    }.cachedIn(viewModelScope)
  • 使用flatMapLatest平滑combineTransFrom。相当于把pageFlow的内层循环拿出来了

局部刷新

场景介绍

  • 授权管理list
  • 点击具体应用,弹出弹窗查看授权详情
  • 在详情页面可分别对基础信息,运动数据授权

建立模型

整体使用了SDK原生的数据结构,其中有两个特点比较影响流模型构建

  • 原生数据结构无法序列化,很难向DialoFragment传递序列化数据作为arguments,需要共用一个ViewModel
  • 原生数据结构无法直接改值
scss 复制代码
// 授权管理列表的uiState,SharingStarted.WhileSubscribed(5000, 1000)第二个参数表示重放的过期保留的时间,默认是MAX_VALUE

val authList = refreshAuthListSignal.transform {

        // 得到授权列表

        emitAll(getSportAuthListUseCase(Unit))

    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000, 1000), Res.Loading)







// 详情弹窗的ui state

// sportAuthorizeInfo为sdk里的数据结构SportAuthorizeInfo

val detailScopes: StateFlow<List<ScopeInfo>> = sportAuthorizeInfo.transform { sportAuthInfo ->

    if (sportAuthInfo != null) {

        XLog.tag(TAG).i("sportAuthInfo = $sportAuthInfo")

        emit(sportAuthInfo.sportAuthScopes.map { it })

    }

}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
  • 局部刷新只会改detailScopes,而不会改sportAuthorizeInfo。所以sportAuthorizeInfo的状态会有一些问题。所以局部刷新尽量不要这么用即sportAuthorizeInfo作为一个stateFlow
  • ScopeInfo 无法直接改值,所以需要自己的数据结构

改造

基于以上模型,做如下改造

kotlin 复制代码
// 用于局部刷新

val partRefreshSignal = MutableSharedFlow<Unit>()



val detailScopes: StateFlow<List<Scope>> = _refreshInfoSignal.transform { action ->

    val pos = if (action is Action.RefreshAllAction) action.pos else _curPos

    val authInfo = curAuthList[pos]

    val scopes = when (action) {

        is Action.RemoveAllAction -> {

            authInfo.sportAuthScopes.map {

                it.newBuilder().name(it.name).isAuthorized(false).build()

            }

        }

        else -> {

            authInfo.sportAuthScopes

        }

    }



    XLog.tag(TAG).i("sportAuthInfo = $authInfo")

    emit(scopes.map { Scope(authInfo.client_key, it.name, it.isAuthorized) })

}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())



fun removeScope(scopeName: String) {

    viewModelScope.launch {

        val scope = curDetailScopes.first { it.name == scopeName }

        val res = removeSportAuthUseCase(RemoveSportAuthUseCase.Params(scope.clientKey, scopeName))

        if (res is Res.Success) {

            XLog.tag(TAG).i("remove sport auth success")

            scope.isAuthorized = false

            XLog.tag(TAG).i(" removeScope scope = $scope ")

            // 局部刷新

            partRefreshSignal.emit(Unit)

            AuthManagerTracker.change_app_authorize(trackerCurPos, curName, "data_detail", toTrackerDataStatus(userInfoStatus), toTrackerDataStatus(sportInfoStatus))

        } else {

            _actionErrorSignal.emit(Unit)

            AuthManagerTracker.change_app_authorize(trackerCurPos, curName, "data_detail", toTrackerDataStatus(userInfoStatus), toTrackerDataStatus(sportInfoStatus))

        }

    }

}



// View层,新增一个collect

launchAndRepeatWithViewLifecycle {

    model.partRefreshSignal.collect {

        binding.bindChecked(model.curDetailScopes)

    }

}
  • google提出的MVI是一个理想模型,在局部刷新场景中很难实现,因为局部数据改动需要让整个数据变化是不科学的,浪费性能。

下载 & 上报

下载和上报这个场景,生命周期和View不太一致,当View退出的时候,两者应该不能被销毁,所以获取流的时候,在第二个参数应该为SharingStarted.Eagerly 如下

scss 复制代码
stateIn(viewModelScope, SharingStarted.Eagerly, SyncState.Init)

这样就会在View退出的时候,上游(生产者)不会被关闭,下游(消费者)会被关闭。当重新进入View的时候,下游会恢复,上游会把状态emit给下游

我这里View是一个loading且不可关闭的view。如果是在可关闭的view场景,逻辑上是需要Service去处理的

在静态类中管理生命周期

我们在常规的开发中可以看到注册与反注册,比如在Activity的onCreate注册,在onDestory反注册,在ViewModel init时注册,在onClear时反注册。但是现在有了```lifecycelScope ``和ViewModelScope`可以已sdk的方式提供出去了。接入方不用考虑生命周期了。比如考虑如下场景

  • 调用方试图在KV组件的Key变化时,触发某些行为,比如Keva
kotlin 复制代码
object UploadHistoryFlagger {

    fun flagHadUploadHistory() {

        KevaUtil.putBoolean(

                Constant.HAD_UPLOAD_HISTORY_KEY + "_" + UserBean.getInstance()?.uniqid,

                true

            )

         hasUploadListener?.invoke()

    }

}

然后在ViewModel申明这样的Listener

kotlin 复制代码
UploadHistoryFlagger.hasUploadListener = {

 //在之前没有上传数据,现在上传数据之后,需要刷新

    XLog.tag(TAG).d("hasUploadListener invoke")

 }



override fun onCleared() {

    // 在ViewModel置null

    UploadHistoryFlagger.hasUploadListener = null

    super.onCleared()

}

改造

我们利用viewModelScopeFlow(本质也是回调)来改造下,即在ViewModelScope的生命周期内用flow监听keva的key的变化,实现如下:

kotlin 复制代码
fun CoroutineScope.kevaChannelFlow(): Flow<String> {

    return getKevaKey()

}



 /**

 * 内部和viewScope的

 */

private fun CoroutineScope.getKevaKey(): Flow<String> {

    val keyShareFlow = MutableSharedFlow<String>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    var listener: Keva.OnChangeListener? = null

    // key回调

    Keva.getRepo(KEVA_MAP_ID, MODE_SINGLE_PROCESS)

        ?.registerChangeListener(Keva.OnChangeListener { repo, key ->

  launch {

 keyShareFlow.tryEmit(key)

            }

 } .also {

 listener = it

 } )

        // 我们在viewModelScope结束的时候反注册

    return keyShareFlow.onCompletion {

 XLog.tag(TAG).i(" onCompletion ")

        Keva.getRepo(KEVA_MAP_ID, MODE_SINGLE_PROCESS)?.unRegisterChangeListener(listener)

    }

}

在ViewModel

scss 复制代码
val kevaDataFlow = viewModelScope.kevaChannelFlow()

    .filter { it == Constant.HAD_UPLOAD_HISTORY_KEY + "_" + UserBean.getInstance()?.uniqid }

init {

    kevaDataFlow.collect {

    XLog.tag(TAG).d("hasUploadListener invoke")

        reload()

    }

}

利用combine的特性,进行复杂的埋点

无法复制加载中的内容

如上图所示,需要紫色流(todayDataFlow)和绿色流(badgeFlow),todayDataFlow是一个定时循环流,两个流都是异步任务。我们需要把埋点时机控制在紫色流的第一个节点和绿色流的节点都发射后触发埋点。难点在于都是异步流,无法确定时机,有可能变成如上图所示的错误流序列

下图说明了如何实现:

无法复制加载中的内容

即使用filter过滤掉2类型节点,然后和绿色流combine即可,如下代码为实现

kotlin 复制代码
 /**

 * trackerBadgeFlow 纯粹是为了埋点

 * 逻辑:等待badgeFlow,todayDataFlow一起返回,才会displayHomePage

 */

private val trackerBadgeFlow = _badgeFlow.transform { badge ->

 Log.i(TAG, "badge: $badge")

    if (badge is Res.Success) {

        if (badge.data != null) emit(badge.data?.status ?: false)

    } else {

        emit(false)

    }

 }



val trackerDisplayHomeFlow = combineTransform(trackerBadgeFlow, todayDataFlow.filter { today ->

 (today is Res.Success && today.data.weekInfo.second > 0)

 } ) { newBadge, today ->

 if (today is Res.Success) {

        val weekInfo = today.data.weekInfo

        Log.i(TAG, "weekInfo: ${weekInfo.second} today = $today")

        if (weekInfo.second > 0) {

            emit(Pair(weekInfo, newBadge))

        }

    }

 } .shareIn(viewModelScope, SharingStarted.WhileSubscribed())



fun fetchBadge(showBadge: Boolean) {

    viewModelScope.launch {

 if (!showBadge) {

            _badgeFlow.emit(Res.Error(IllegalArgumentException()))

        } else {

            val res = getBadgeInfoUseCase(Unit)

            _badgeFlow.emit(res)

        }

    }

}

生命周期生效逻辑

上游会以下游得生命周期Eagerly为准,其实比较类似RxJava

总结

重新思考响应式 vs 命令式

无法复制加载中的内容

  • 响应式面向流编程,监听流的变化,响应变化
  • 用声明的方式响应未来发生的事件流
  • 响应式面向结果,命令式面向过程
  • Flutter在界面布局上是不能通过命令式去写的,只能在布局上描述结果。然后监听布局相关的数据变化,从而改变UI
  • 响应式类似y=f(x),
  1. 所以基于shareFlow和stateFlow的ViewModel类一般长这样(类似Flutter,Compose的写法)
kotlin 复制代码
val detailScopes: StateFlow<List<Scope>> = _refreshInfoSignal.transform { action ->

 val pos = if (action is Action.RefreshAllAction) action.pos else _curPos

    val authInfo = curAuthList.getOrNull(curPos) ?: return@transform

    val scopes = when (action) {

        is Action.RemoveAllAction -> {

            authInfo.sportAuthScopes.map {

 it.newBuilder().name(it.name).isAuthorized(false).build()

            }

 }

        else -> {

            authInfo.sportAuthScopes

        }

    }



    XLog.tag(TAG).i("sportAuthInfo = $authInfo")

    emit(scopes.map { Scope(authInfo.client_key, it.name, it.isAuthorized) } )

 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
  • detailScopes描述了结果
  • _refreshInfoSignal描述了输入
  • transform一般为转换函数
  1. LiveData的写法一般长这样
kotlin 复制代码
val detailScopes = MutableLiveData<List<Scope>>()



fun removeAllAction() {

     val scopes = authInfo.sportAuthScopes.map {

 it.newBuilder().name(it.name).isAuthorized(false).build()

            }

     detailScopes.value = scopes

}



fun refreshAllAction() {

     //   

     detailScopes.value = scopes

}

可以看到两种写法的不同点

  • 第一种写法detailScopes是一个结果,可以直接定义。第二种写法需要命令式的写detailScopes.value = scopes

在普通的android开发中使用申明式的写法是非常理想化的。因为其不具备类似flutter和compose的树形结构(该结构会提高UI的刷新效率),所以一般是会有多个uiState用来绑定每个部分的ui。

相关推荐
CYRUS STUDIO2 分钟前
Android APP 热修复原理
android·app·frida·hotfix·热修复
火柴就是我1 小时前
首次使用Android Studio时,http proxy,gradle问题解决
android
limingade1 小时前
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
android·智能手机·电脑·蓝牙电话·电脑打电话
浩浩测试一下1 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
青春给了狗3 小时前
Android 14 系统统一修改app启动时图标大小和圆角
android
pengyu4 小时前
【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的"魔法"✨
android·flutter·dart
居然是阿宋5 小时前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin
凉、介5 小时前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci
小贾要学习6 小时前
【C++】继承----下篇
android·java·c++
投笔丶从戎8 小时前
Kotlin Multiplatform--01:项目结构基础
android·开发语言·kotlin