kotlin协程系列:callbackFlow

1. 背景

callbackFlow 主要用于回调场景,它可以将回调 api 转化为 flow。

本文通过一个回调案例的不同实现,来体现 callbackFlow 的作用。

2. 回调场景典型写法

假设合作方给我们提供了一个接口 ICustomerStateManager,用来注册监听用户状态 CustomerState 的变化。

每当用户状态变化,Manager 都会通知所有 Listener。

使用方如果要监听状态变化,需要向 ICustomerStateManager 注册 Listener。

有一个 Fragment 页面需要 根据不同的用户状态,展示不同的 UI。根据 官方架构指南,我们用 ViewModel 封装该页面的业务逻辑,对外暴露 ui 数据流。

kotlin 复制代码
class TestViewModel : ViewModel() {

    private val _uiState: MutableStateFlow<UIState> = MutableStateFlow(
        CustomerStateManager.getCurrentState().toUIState()
    )
    val uiState: Flow<UIState> = _uiState

    private val customerStateListener = object : ICustomerStateListener {
        override fun onStateChanged(newCustomerState: CustomerState) {
            // 用户状态变化时,重组UI数据并发射
            val newUIState = newCustomerState.toUIState()
            _uiState.value = newUIState
        }
    }

    fun onFragmentVisible() {
        // Fragment可见时,注册监听用户状态的变化
        CustomerStateManager.registerListener(customerStateListener)
    }

    fun onFragmentInVisible() {
        // Fragment不可见时,反注册,防止内存泄漏
        CustomerStateManager.unregisterListener(customerStateListener)
    }
}

data class UIState(
    val customerState: CustomerState
    // ...
)

private fun CustomerState.toUIState(): UIState {
    return UIState(
        customerState = this, 
        // ...
    )
}

Fragment 需要将 visible、inVisible 两个事件发生时通知 ViewModel 注册与反注册;

同时,Fragment 在可见时监听 ViewModel 的 UI 状态流,以此更新 UI。

kotlin 复制代码
class TestFragment : : Fragment() {

    private val viewModel: TestViewModel by viewModels<TestViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // 可见性变化时通知 ViewModel
        lifecycleOwnerForView.lifecycle.addObserver(object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                when (event) {
                    Lifecycle.Event.ON_RESUME -> {
                        viewModel.onFragmentVisible()
                    }
                    Lifecycle.Event.ON_PAUSE -> {
                        viewModel.onFragmentInVisible()
                    }
                    Lifecycle.Event.ON_DESTROY -> {
                        source.lifecycle.removeObserver(this)
                    }
                    else -> Unit
                }
            }
        })
        
        // 监听UI状态,更新UI
        viewModel.uiState
            .onEach { uiState: UIState ->
                // 更新UI...
            }
            .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED)
            .launchIn(viewLifecycleOwner.lifecycleScope)
    }
}

思考一下

  • Fragment 在 Resume 时,需要监听 uiState 的变化;同时,Resume 时还要通知 ViewModel 监听用户状态的变化。
  • 假如我们能实现:Fragment 开始监听 uiState 时,上游自动监听用户状态的变化;Fragment 停止监听 uiState 时,上游取消监听用户状态的变化。
  • 那我们是不是就不需要将 visible、invisible 通知给 ViewModel 了?那可以省很多代码。

3. 使用 flow 实现 ❌

聪明的你肯定能想到,flow 是冷流,下游收集的时候才开始执行 flow {...},下游收集完成时会执行 onCompletion {...}

假如我们在 flow {...} 中注册监听,在 onCompletion {...} 时取消监听,是不是就能实现上面的诉求呢?于是写出代码:

kotlin 复制代码
class TestViewModel : ViewModel() {

    private var customerStateListener: ICustomerStateListener? = null
    
    val uiState: Flow<UIState> = flow {
        customerStateListener = object : ICustomerStateListener {
            override fun onStateChanged(newCustomerState: CustomerState) {
                emit(newCustomerState.toUIState())
            }
        }
        CustomerStateManager.registerListener(customerStateListener!!)
    
        // ...
    }
    .onCompletion {
        customerStateListener?.let { CustomerStateManager.unregisterListener(it) }
    }
}

这会有几个问题:

  1. 无法持续监听

flow {...} 中的代码执行完后,流就结束了。尽管下游 Fragment 还在收集,也无法再监听用户状态的变化。

  1. emit 需在协程作用域中

编译报错。提示 emit 需要在协程作用域中进行,无法在回调中发射。

  1. emit 需在 flow 协程作用域中

为了解决前两个问题,我们将代码进行调整。

kotlin 复制代码
class TestViewModel : ViewModel() {

    private var customerStateListener: ICustomerStateListener? = null

    val uiState: Flow<UIState> = flow {
        customerStateListener = object : ICustomerStateListener {
            override fun onStateChanged(newCustomerState: CustomerState) {
                // 开启新的协程域发射
                CoroutineScope(Dispatchers.IO).launch {
                    emit(newCustomerState.toUIState())
                }
            }
        }
        CustomerStateManager.registerListener(customerStateListener!!)

        // delay一段时间, 防止协程直接结束
        delay(10000)
    }
    .onCompletion {
        customerStateListener?.let { CustomerStateManager.unregisterListener(it) }
    }
}

运行时报错。提示 emit 不可以在其他协程域执行。

试想下,假如 flow 支持在别的协程域 emit:

  • 多个线程同时 emit,多个线程同时触发下游收集者 collect,是不是可能造成 线程安全问题
  • 其他协程持有了 emit 所需的上下文,这个上下文可能包含了对 flow 及 ViewModel 的引用,假如其他协程迟迟不结束,是不是可能造成 内存泄漏

4. 使用 callbackFlow ✔

为了在回调场景中,要想 持续监听 用户状态变化,同时能在 非flow协程域 中发射数据,我们可以使用官方提供的 callbackFlow

1. 简单使用

这是使用 callbackFlow 后,ViewModel 的代码。

kotlin 复制代码
class TestViewModel : ViewModel() {

    val uiState: Flow<UIState> = callbackFlow {
        trySend(CustomerStateManager.getCurrentState().toUIState()) // 发射初始状态

        val customerStateListener = object : ICustomerStateListener {
            override fun onStateChanged(newCustomerState: CustomerState) {
                trySend(newCustomerState.toUIState()) // 1
            }
        }
        CustomerStateManager.registerListener(customerStateListener)

➡      awaitClose { // 2
            CustomerStateManager.unregisterListener(customerStateListener)
        }
    }
}

观察注释的地方:

  • 注释 // 1 ,可以在 非flow协程域 中发射数据了。
  • 注释 // 2 ,这是一个 挂起函数 ,会一直挂起,直到 主动 close下游结束收集 ,才会执行 lambda 中的代码。也就是说,现在可以 持续监听 了。

再看看调整后,Fragment 的代码。

kotlin 复制代码
class TestFragment : : Fragment() {

    private val viewModel: TestViewModel by viewModels<TestViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.uiState
            .onEach { uiState: UIState ->
                // 更新UI...
            }
            .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED)
            .launchIn(viewLifecycleOwner.lifecycleScope)
    }
}

可以看到调整后,Fragment 不需要再将 visible 和 invisible 时机通知 ViewModel 了,代码轻量了许多。

除此之外,将回调变成 flow 后,可以更好的运用协程和 flow 的操作符了。

2. 原理浅析

这里结合 callback官方文档 来讲解。

观察图中标注的地方:

  • // 1: callbackFlow 是 冷流,即下游收集时才开始执行。
  • // 2: 发射的元素都会被送到 Channel(管道) 中,收集者会从管道依次取出元素消费。这使得它允许在不同的上下文或协程域中发射,也不会有第3节提到的的线程安全问题。
  • // 3/4/5: 使用方利用 awaitClose 来执行清除操作,从而避免内存泄漏。它会一直挂起,直到 手动调用 close下游结束收集。挂起恢复后,会执行 lambda 中的代码。

3. 缓存

当 callbackFlow 的 发射速度大于消费速度 时,新发射的数据会放到缓存中。

如果缓存满了,会根据缓存策略处理新发射的数据。对于 trySend(...) 来说,缓存策略及对应的表现为:

缓存策略 表现
SUSPEND 直接丢弃新发射的数据
DROP_OLDEST 丢弃缓存中最旧的数据
DROP_LATEST 丢弃缓存中最新的数据

默认情况下,callbackFlow 的缓存大小为 64 ,缓存策略为 SUSPEND

kotlin 复制代码
private class CallbackFlowBuilder<T>(
    // ...
    capacity: Int = BUFFERED ,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ChannelFlowBuilder<T>(block , context , capacity , onBufferOverflow) { ... }

如果要调整缓存大小以及缓存策略,可以使用 buffer(...) 方法。

kotlin 复制代码
class TestViewModel : ViewModel() {

    val uiState: Flow<UIState> = callbackFlow {
        // ...
    }
        .buffer(
            capacity = 2 ,
            onBufferOverflow = BufferOverflow.DROP_OLDEST
        )
}
相关推荐
alexhilton31 分钟前
Compose Unstyled:Compose UI中失传的设计系统层
android·kotlin·android jetpack
刘龙超2 小时前
如何应对 Android 面试官 -> 玩转 RxJava (基础使用)
android·rxjava
柿蒂3 小时前
从动态缩放自定义View,聊聊为什么不要把问题复杂化
android·ai编程·android jetpack
没有了遇见5 小时前
Android 原生定位实现(替代融合定位收费,获取经纬度方案)
android·kotlin
一枚小小程序员哈5 小时前
基于Android的车位预售预租APP/基于Android的车位租赁系统APP/基于Android的车位管理系统APP
android·spring boot·后端·struts·spring·java-ee·maven
诸神黄昏EX5 小时前
Android SystemServer 系列专题【篇四:SystemServerInitThreadPool线程池管理】
android
用户2018792831676 小时前
pm path 和 dumpsys package 的区别
android
是店小二呀6 小时前
【C++】智能指针底层原理:引用计数与资源管理机制
android·java·c++
DoubleYellowIce7 小时前
一次混淆XLog导致的crash分析记录
android