Kotlin Flow 一种更安全的 UI 层收集流的方式

59 篇文章 0 订阅
11 篇文章 0 订阅

前言

在我们的 Android App, Kotlin flows 通常用来收集 UI 层需要展示的那些数据. 但是你在收集数据的时候, 你得确保它不会做很多额外的事情、不会浪费资源、不会因为视图层退到后台或者销毁而引起内存泄漏.

正因为 Kotlin flows 和 RxJava 都可能有上述的问题, 所以官方的 LiveData 是一个比较好的选择. 但是 LiveData 的局限性比较大, 它缺少了 flows 和 RxJava 的可组合性, 也缺少了很多的好用的链式操作符的支持.

所以本文就是介绍如何利用 Kotlin flows 收集数据, 并且拥有 LiveData 的特性. 也支持各种组合和变换的能力.

下文会介绍如何使用 Lifecycle.repeatOnLifecycleFlow.flowWithLifecycle 的 API 来避免资源的浪费. 并且解释它为什么是一种 UI 层数据收集的好方式

资源浪费

在平常的编码中, 我们推荐从架构的较低的层面就开始提供 Flow 这种 Api 出来给上层使用. 但是你得保证正确的去收集/订阅他们.

一个由 channel 或者使用操作符(比如:buffer, conflate, flowOn 或者 shareIn)做缓冲的冷流. 当使用一些存在的 Api(比如:CoroutineScope.launch, Flow.launchIn, 或者 LifecycleCoroutineScope.launchWhenX) 去收集是不安全的. 除非你手动的取消 Job, 当 Activity 进入到后台的时候. 这些 Api 会让 flow 底层的数据生产者一直在后台保持活跃, 并且浪费资源

Note: 一个冷的 flow 是一种 flow, 它在有新的订阅者收集/订阅的时候执行生产者的代码块来产生数据.

举例一个官方的例子:使用 callbackFlow 来不断发射位置更新的信息

// 给 FusedLocationProviderClient 这个类扩展一个 locationFlow() 方法, 返回值是 Flow
// 实现 callbackFlow
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    // 位置信息更新的 callback
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return // 如果为空返回
            // 尝试发送数据
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    // 执行位置的更新
    requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
        .addOnFailureListener { e ->
            close(e) // in case of exception, close the Flow
        }
    // 这是一个挂起函数, 当 flow 被关闭的时候 block 中的代码会被执行
    awaitClose {
      	// 移除监听
        removeLocationUpdates(callback)
    }
}

从 UI 层收集/订阅这个 Flow, 使用上述说的其中一个 Api 去启动, 都会导致位置信息还会不断的更新即使 View 在屏幕上没有展示或者进入了后台.

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 当状态至少是 STARTED 的时候会触发
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
        
    }
}

为了解决这个问题, 你可能需要手动去取消当你的 View 到了后台. 避免位置信息的提供者一直在发送造成资源浪费. 比如, 你可能需要下面这样做:

class LocationActivity : AppCompatActivity() {
    // Coroutine listening for Locations
    private var locationUpdatesJob: Job? = null
    override fun onStart() {
        super.onStart()
        locationUpdatesJob = lifecycleScope.launch {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
    }

    override fun onStop() {
        // Stop collecting when the View goes to the background
        locationUpdatesJob?.cancel()
        super.onStop()
    }
}

这是一种好的处理方式. 但是这些代码过于模板和不友好了. 对于我们开发者来说, 我们是拒绝写模板代码的. 另外不写样板代码还有一个很大的好处就是. 少写了代码, 就一定可以制造更少的错误!

Lifecycle.repeatOnLifecycle

现在我们需要来解决这些问题. 需要满足两点:

  1. 使用足够简单
  2. Api 友好, 并且易于理解和记忆
  3. 最重要的一点是安全!它也应该支持任何场景的 Flow, 不管 Flow 的实现细节是怎么样的.

在 lifecycle-runtime-ktx 库中支持了 Lifecycle.repeatOnLifecycle 让我们来看一下下面的代码:

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 创建一个协程
        lifecycleScope.launch {
            // The block passed to repeatOnLifecycle is executed when the lifecycle
            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
            // It automatically restarts the block when the lifecycle is STARTED again.
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Safely collect from locationFlow when the lifecycle is STARTED
                // and stops collection when the lifecycle is STOPPED
                locationProvider.locationFlow().collect {
                    // New location! Update the map
                }
            }
        }
    }
}

repeatOnLifecycle 是一个 suspend 标记的挂起方法, 他有一个 Lifecycle.State 参数

当 lifecycle 的状态达到了指定的状态, 它会自动创建并且启动一个新的协程去执行指定的 block. 并且在状态扭转低于所给的状态后自动取消协程.

这样子可以很好的避免写模板代码. 你可以猜得到的是, 这 Api 的需要在 activity 的 onCreate 或者 fragment 的 onViewCreated 方法中去执行. 这样可以避免产生位置的异常行为. 在 Fragment 中可以这样写.

class LocationFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ...
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                locationProvider.locationFlow().collect {
                    // New location! Update the map
                }
            }
            // 这里的代码除非状态 destroyed 了否则不会执行
        }
    }
}

重要提示:Fragment 应该始终使用 viewLifecycleOwner 去触发 UI 更新. 但是这不适用于 DialogFragment. 因为它可能没有 View. 对于 DialogFragment. 你应该使用 lifecycleOwner

简单说下原理

repeatOnLifecycle 是一个挂起方法. 当状态达到了指定的值, 内部会启动一个 协程去执行 block 中的代码. 当状态低于给定的状态, 会取消协程从而取消 Flow 的收集/订阅. 因为内部一直需要监测状态来启动和取消. 所以 repeatOnLifecycle 方法下方的代码只有状态到达了 destroyed, 那么才能得到执行. 具体实现代码如下:

用图来描述一下

repeatOnLifecycle 可以防止你浪费资源并且防止 app 崩溃(因为在不合适的 lifecycle 的状态下收到了数据更新的时候)

Flow.flowWithLifecycle

你也可以使用 Flow.flowWithLifecycle 操作符当你只有一个 Flow 需要收集/订阅的时候. 这个 API 使用 repeatOnLifecycle 作为底层的实现.

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 收集一个 Flow 的用法
        lifecycleScope.launch {
            locationProvider.locationFlow()
                .flowWithLifecycle(this, Lifecycle.State.STARTED)
                .collect {
                    // New location! Update the map
                }
        }
        
        // 收集多个 FLow 的用法
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    flow1.collect { /* Do something */ }   
                }
                
                launch {
                    flow2.collect { /* Do something */ }
                }
            }
        }
    }
}

底层的生产者

就算你使用这些 Api, 如果你收集/订阅到了 Hot Flow, 那么即使你没有人去收集/订阅它, 它还是会浪费资源.

但是这也是有好处的, 在后续的收集/订阅发生之后, 订阅者总能拿到最新的数据去显示, 而不是拿到老旧的数据. 但是如果你确实不必要它一直保持活跃. 那么这里有一些措施可以防止一下.

MutableStateFlow 及其 Api 提供了一个 subscriptionCount 的字段, 你可以根据这个字段去判断是否生产数据.

和 LiveData 的对比

你可能发现了这些 Api 的行为和 LiveData 真是太像了. 这是事实! LiveData 能感知生命周期, 它的行为是一种理想的方式从 UI 层去订阅数据流. 和 Lifecycle.repeatOnLifecycleFlow.flowWithLifecycle APIs 类似

使用这些 Api 去代替 LiveData 是 Kotlin 项目独有的. 如果你使用这些 Api 去收集, LiveData 没有任何优势, 相比于协程和 Flow. 毕竟 Flow 可以有更多的组合性和变换性, 还可以从任何一个 Dispatcher 去收集数据. 并且支持很多的操作符去满足各种场景的需求. 反观 LiveData, 只有极少的操作符可用. 并且只能从 UI 线程去订阅.

data binding 支持了 StateFlow

另外的, 你使用 LiveData 的一个可能的原因是因为它支持 data binding. 那么现在, StateFlow 它也支持啦. 更多的相关信息可以查看这里

总结

使用 Lifecycle.repeatOnLifecycle 或者 Flow.flowWithLifecycle 的 Api 可更安全的去收集 UI 层想要的数据

看都看完了. 关注一下公众号呗

有任何问题, 欢迎留言

原文链接

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值