从MVI架构中学习Kotlin Flow的几种特性与用法

36 篇文章 3 订阅

Kotlin Flow 的几种特性与用法

前言

之前的 MVI 架构一文中,有小伙伴会有疑问,为什么要用这个Flow,能不能平替其他的 Flow 。

其实在 MVI 的封装中,每一种 Flow 都用到了,特别适合对比学习各种 Flow 的各种特性。

如果你对 MVI 的架构不太了解,可以看看我之前的文章:

遍历全网Android-MVI架构,从简单到复杂学习总结一波

2024年Android开发架构推荐

我们直接先上代码,然后再一一讲解区别。

abstract class BaseEISViewModel<E : IUIEffect, I : IUiIntent, S : IUiState> : ViewModel() {

 private var _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<S> = _uiStateFlow

    //页面事件的 Channel 分发
    private val _uiIntentFlow = Channel<I>(Channel.UNLIMITED)

    //更新State页面状态 (data class 类型)
    fun updateUiState(reducer: S.() -> S) {
        _uiStateFlow.update { reducer(_uiStateFlow.value) }
    }

    //更新State页面状态 (sealed class 类型)
    fun sendUiState(s: S) {
        _uiStateFlow.value = s
    }

    //发送页面事件
    fun sendUiIntent(uiIntent: I) {
        viewModelScope.launch {
            _uiIntentFlow.send(uiIntent)
        }
    }

    init {
        // 这里是通过Channel的方式自动分发的。
        viewModelScope.launch {
            //收集意图 (观察者模式改变之后就自动更新)用于协程通信的,所以需要在协程中调用
            _uiIntentFlow.consumeAsFlow().collect { intent ->
                handleIntent(intent)
            }
        }

    }

    //每个页面的 UiState 都不相同,必须实自己去创建
    protected abstract fun initUiState(): S

    //每个页面处理的 UiIntent 都不同,必须实现自己页面对应的状态处理
    protected abstract fun handleIntent(intent: I)

    //一次性事件,无需更新
    private val _effectFlow = MutableSharedFlow<E>()
    val uiEffectFlow: SharedFlow<E> by lazy { _effectFlow.asSharedFlow() }

    //两种方式发射,在协程外用viewModelScope发射
    protected fun sendEffect(builder: suspend () -> E?) = viewModelScope.launch {
        builder()?.let { _effectFlow.emit(it) }
    }

    //两种方式发射,suspend 协程中直接发射
    protected suspend fun sendEffect(effect: E) = _effectFlow.emit(effect)

}

代码很清晰,注释很详细,接下来我们就看看他们的区别。

一、普通 Flow 的特性

Kotlin 的 Flow 是一种冷流(Cold Flow),这意味着它只有在被收集时才开始发射数据,这与热流(如 StateFlow 和 SharedFlow)形成对比,热流无论是否有收集器都会发射数据。

Flow 是序列生成和处理的强大工具,能够轻松地进行转换和组合。适用于数据的异步获取和处理,在数据到来时发射数据。

例如:

fun fetchUserData(): Flow<UserData> = flow {
    // 假设这是从数据库或网络获取用户数据的操作
    val data = database.getUserData()
    emit(data) // 发射数据
}

然后,在你的 Activity 或 Fragment 中,你可以这样收集这个 Flow:

viewModel.fetchUserData().onEach { userData ->
    // 更新UI
}.launchIn(lifecycleScope) // 使用lifecycleScope确保在合适的生命周期内收集

适合的场景:

  • 数据流的异步处理:当你需要从数据库、网络或其他异步源获取数据时,Flow 是一个很好的选择。

  • 连续数据的处理:如果你的应用需要处理一系列连续的数据(例如,实时更新的数据),Flow 可以帮助你以声明式的方式处理这些数据流。

  • 转换和组合数据:Flow 提供了丰富的操作符,如 map、filter、combine 等,这些操作符可以帮助你轻松地转换和组合数据流。

特点

  • 冷流:Flow 是冷流,它只在有收集器时开始发射数据,这意味着数据是按需产生的。

  • 轻量级:Flow 设计用于处理数据流,与 LiveData 相比,它更轻量级,没有生命周期感知的特性,这使得它在 ViewModel 中使用更为灵活。

  • 支持协程:Flow 完全集成了 Kotlin 协程,这使得它在处理异步操作时非常强大且易于使用。

  • 丰富的操作符:Flow 提供了一系列操作符,用于数据的转换、过滤、组合等操作,这些操作符使得 Flow 在处理复杂的数据流时非常灵活。

除了常规的 Flow,我们主要的分析还是基于以下的几种热流。

二、StateFlow 的特性

StateFlow 是一种特殊的 Flow,它用于持有状态。它总是有一个初始值,并且只会在状态有变化时发射新的值。它是热流(Hot Flow),意味着当有多个收集器时,它们会共享同一个状态,并且只有最新的状态会被发射。适用于表示UI状态,因为UI总是需要知道当前的状态是什么。

为什么页面的状态用StateFlow来发送和监听,有什么道理吗?

  1. 状态保留 StateFlow 自动保持其最新值的状态。这意味着每当有新的收集者开始收集此流时,它会立即接收到最新状态的最新值。这对于 UI 编程尤其重要,因为您通常希望 UI 组件(如Activity、Fragment或View)能够立即反映当前的状态,即使它们在状态更新后才开始观察状态。

  2. 去重 StateFlow 仅在状态发生变化时通知收集者。如果您向 StateFlow 发射一个与当前值相同的值,这个值将不会被重新发射给收集者。这有助于减少不必要的 UI 更新和性能开销,因为您的 UI 组件不会对相同的状态重复渲染。

  3. 线程安全 StateFlow 的操作是线程安全的,确保即使在并发环境中,状态的更新和读取也保持一致性。在复杂的应用程序中,可能有多个协程同时尝试更新状态,StateFlow 保证了这种操作的正确性。

使用默认的SharedFlow 和 普通的Flow冷流不行吗?

  • SharedFlow:虽然 SharedFlow 可以高度自定义,包括配置重播和缓存策略,但它不保证自动保持和重放最新状态。如果使用 SharedFlow,您需要手动管理状态的保留和更新,这增加了复杂性。

  • 普通 Flow:普通的 Flow 是一个冷流,意味着它不保持状态,并且每次有新的收集者时,数据的产生逻辑都会从头开始。这使得它不适合作为表示 UI 状态的机制,因为您通常希望即使在数据生产后也能让后来的收集者立即获得最新状态。

三、Channel 的特性

Channel 类似于阻塞队列,但它是挂起的,适用于协程之间的通信。它可以配置为不同的模式,如缓存大小和发送行为。适用于事件的生产者-消费者模型,如任务执行、消息传递等。

为什么 sendUiIntent 中接收 UI 的驱动事件要用 Channel 来发送和监听,有什么道理吗?

  1. 事件的即时性和一次性 Channel 被设计为用于通信的原语,特别适合于处理一次性事件或命令,这些事件或命令通常不需要被重复消费或保留状态。在 UI 交互中,用户的操作(如点击、滑动等)往往是即时和一次性的,Channel 能够有效地传递这些即时事件,确保它们被及时处理。

  2. 缓冲和背压管理 Channel 提供了不同的缓冲策略,包括无限缓冲(Channel.UNLIMITED)、有界缓冲和不缓冲(Channel.RENDEZVOUS)。这使得开发者可以根据具体的应用场景选择最合适的策略来处理事件流,例如,通过使用无限缓冲,可以确保在高频事件发生时不会丢失任何事件。

  3. 明确的消费模式 与 Flow 相比,Channel 提供了更明确的消费模式,即发送方和接收方。这种模式使得事件的发送和接收更加直观,尤其是在需要明确处理每个事件的场景中。此外,Channel 的 send 和 receive 操作可以很容易地集成到协程中,提供了更灵活的并发处理能力。

使用默认的SharedFlow 和 StateFlow,普通的Flow冷流不行吗?

  • SharedFlow:虽然 SharedFlow 可以用于处理事件,并且支持配置重播和并发策略,但它更适合于需要多个观察者共享和重播事件的场景。对于一次性的、即时的 UI 事件,SharedFlow 的这些特性可能并不是必需的。

  • StateFlow:StateFlow 主要用于表示和观察可变状态,它保留最新的状态值并且只在状态变化时通知观察者。这种特性使得 StateFlow 不适合用于传递一次性的 UI 事件。

  • 普通 Flow:普通的 Flow 是一个冷流,它不保留状态或事件,而是在每次收集时重新开始生成数据。这种特性使得它不适合于处理即时的 UI 事件,因为事件可能会在观察者开始收集之前发生并且丢失。

综上所述,Channel 在处理即时和一次性的 UI 事件方面提供了特定的优势,尤其是在需要明确的事件发送和接收、以及灵活的缓冲和背压管理时。这些特性使得 Channel 成为在特定场景下处理 UI 事件的理想选择。

四、SharedFlow 的特性

SharedFlow 也是一种热流,能够向多个收集器广播事件。它提供了更灵活的配置,比如可以配置重播(replay)的值的数量,以及在没有收集器的情况下保留值的能力。适用于一次性事件、消息广播等场景。

默认的 SharedFlow

为什么UI的效果通知要用 SharedFlow 来发送和监听,有什么道理吗?

  1. 多观察者支持 与 StateFlow 和普通的 Flow 相比,SharedFlow 支持多个观察者同时订阅事件,而且每个观察者都会收到独立的事件流。这一点对于 UI 事件非常重要,因为可能有多个组件或功能同时对同一事件感兴趣,并且需要独立处理这些事件。

  2. 事件重播和缓存策略 SharedFlow 可以配置事件的重播 (Replay) 和缓存 (Buffer) 策略。这意味着你可以控制新订阅者接收多少最近的事件,或者当流的发射速度超过处理速度时如何缓存事件。这在处理 UI 事件时非常有用,例如,当你希望新加入的观察者能够接收到最近的状态或事件时。

  3. 精细的背压管理 尽管 Channel 也提供了背压管理,但 SharedFlow 允许更精细的背压控制,特别是在配置缓存和重播行为时。这对于确保 UI 事件不会因为过载而丢失或导致性能问题非常关键。

使用Channel 和 StateFlow,普通的Flow冷流不行吗?

  • Channel:虽然 Channel 适用于一次性事件和瞬时通信,但其主要设计用于协程之间的通信,而不是状态管理或多观察者场景。Channel 的每个事件只能被一个观察者消费,这限制了其在 UI 事件广播中的使用。

  • StateFlow:StateFlow 适用于状态管理,因为它总是保留最新的状态值,并且能够向新订阅者重播这个状态。然而,它不适合用于表示可以发生多次的一次性事件,如点击事件。

  • 普通 Flow:普通的 Flow 是一个冷流,它只有在收集时才开始发射数据。这意味着它不适合用于事件的多订阅者广播或需要重播最近事件给新订阅者的场景。

综上所述,SharedFlow 在用于 UI 效果通知时提供了对多观察者支持、可配置的重播和缓存策略以及精细的背压管理,这些特性使得它成为处理这些场景的理想选择。

配置版 SharedFlow

SharedFlow 是 Kotlin 协程中最强大的 Flow 实现,他有很多的配置,具有最大的灵活性和可定制性。我们可以通过设置不同的参数和处理策略,来模拟 StateFlow 和 Channel 的行为。

模拟 StateFlow

StateFlow 持有一个值,并且只在值改变时通知观察者。要用 SharedFlow 模拟这个行为,我们可以配置它的重播缓存(replay cache)大小为 1,并且设置它的行为,使它只在值改变时发射数据。

val sharedFlowState = MutableSharedFlow<Int>(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
).apply {
    tryEmit(initialValue)  // 初始化SharedFlow的值
}

// 对比StateFlow
val stateFlow = MutableStateFlow(initialValue)

使用 tryEmit 来设置初始值,这样就模拟了 StateFlow 的行为。接下来,您需要确保只有在值改变时才调用 emit 方法。

模拟 Channel

Channel 用于协程间的通信,并且可以配置为有不同的容量。用 SharedFlow 来模拟 Channel,您可以设置它的重播值为 0 并配置缓存策略。

val sharedFlowChannel = MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = Channel.BUFFERED.capacity, // 或者设置具体的数值
    onBufferOverflow = BufferOverflow.SUSPEND
)

// 对比Channel
val channel = Channel<Int>(Channel.BUFFERED)

示例:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

// 模拟StateFlow
fun simulateStateFlow() = MutableSharedFlow<Int>(replay = 1)

// 模拟Channel
fun simulateChannel() = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.SUSPEND)

fun main() = runBlocking {
    val simulatedStateFlow = simulateStateFlow()
    simulatedStateFlow.tryEmit(1) // 设置初始值

    launch {
        simulatedStateFlow.collect { value ->
            println("StateFlow simulated: Received $value")
        }
    }
    simulatedStateFlow.emit(2) // 发送新值
    simulatedStateFlow.emit(2) // 发送相同的值,不会再次通知

    val simulatedChannel = simulateChannel()

    launch {
        simulatedChannel.collect { value ->
            println("Channel simulated: Received $value")
        }
    }
    simulatedChannel.emit(1) // 发送值
    simulatedChannel.emit(2) // 发送另一个值

    delay(1000) // 等待收集
}

后记

本文我们分别说明普通Flow,StateFlow,SharedFlow,Channel的特性和他们的差异,以及为什么 MVI 场景下要选用对应的 Flow 来完成框架。

具体的分析可以在文中查看,那么我们接下来再看一个问题,如果此时 Activity 销毁重建,那么Channel 的驱动事件,StateFlow的UI状态,和SharedFlow 的UI效果,会有怎样的效果呢?

在 Android 应用中,Activity 的销毁和重建(例如,由于配置更改)会对使用 Channel、StateFlow 和 SharedFlow 的事件和状态管理产生不同的影响。我们可以分别讨论它们的行为:

  1. StateFlow(UI状态) StateFlow 保持其状态,即便是当 Activity 销毁并重建。这意味着新的 Activity 实例订阅 uiStateFlow 时,将立即接收到当前的状态,也就是获取了当前状态的一个快照,确保 UI 正确反映了最新的状态。所以我们会根据UI状态刷新对应的布局展示,符合我们的预期。

  2. Channel(驱动事件) Channel 用于处理一次性的事件或者命令,它是一个热流,意味着一旦事件被发送并被收集,该事件就消失了。如果 Activity 在事件发送后被销毁并重建,除非 ViewModel 重新发送事件,否则新的 Activity 实例不会接收到之前的事件。因此不会重新驱动事件,符合我们的预期。

  3. SharedFlow(UI效果) 对于 SharedFlow,其行为取决于你对它的配置,尤其是它的重播策略。在 BaseEISViewModel 中,MutableSharedFlow 被初始化时没有指定重播或缓冲策略,因此默认情况下它不会重播旧的事件给新的订阅者。这意味着,在 Activity 重建时,只要没有新的事件被发送,就不会出现重复触发的情况。所以包括页面导航,页面弹窗,吐司等UI效果也不会触发,符合我们的预期。

总结:

StateFlow 用于持续状态的管理,保证了状态的一致性,并且默认保存有状态的一个快照,重建之后也能恢复,特别适合 UI 状态的管理。

Channel 主要处理一次性事件,一旦事件被收集,它就不会再次触发,除非显式地重新发送,特别适合 UI 事件的驱动

SharedFlow 由于默认配置并没有配置重播策略,则不会导致重复触发问题,特别适合 UI 效果的管理。

Ok,文章到此就告一段落,本文代码都已在文中贴出,内部代码参考项目为 【2024年Android项目开发模板开源】

作者:Newki
链接:https://juejin.cn/post/7371318721905262618
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值