【业务场景架构实战】5. 使用 Flow 模式传递状态过程中的思考点

天下难事,必作于易;天下大事,必作于细。

我在使用 Kotlin Flow 实现业务功能模块的过程中,遇到一些细节上的问题,虽然微小,但仍值得思考、学习和记录。请将本文结合前一篇文章《【业务场景架构实战】4. 支付状态分层流转的设计和实现》共同阅读。

1. 如何将 支付 SDK 的回调式接口封装为 Flow

这是一个典型场景,把 传统回调 -> Flow 的编程风格进行转换,其核心思想是用 callbackFlow {},将异步回调改写为同步的格式。

异步 API

kotlin 复制代码
fun pay(orderId: String, callback: (result: PaymentResult) -> Unit)

改写后的 Flow,便于进行状态监听

kotlin 复制代码
class AlipaySdkWrapper : PaymentSdkWrapper {
    override fun pay(orderId: String): Flow<PaymentResult> = callbackFlow {
        // 假设这是支付宝SDK提供的函数
        AlipaySdk.pay(orderId) { result ->
            trySend(result) // 将回调结果发送到Flow
            close()         // 一次性回调,结果出来就关闭流
        }

        // 当Flow被取消/关闭时,执行清理逻辑
        awaitClose {
            // 比如取消支付请求(如果SDK支持)
            // AlipaySdk.cancel(orderId)
        }
    }
}

关于 callbackFlow,还有一些细节需要留意:

  • 内部使用 trySend() 向下游发射数据。
  • 默认是 冷流 ,只有新产生订阅者时,才会触发 AlipaySdk.pay(),这也符合支付接口原本的设计意图。
  • 如果支付只有一次最终结果(成功/失败),则可使用 close() 对流执行关闭操作。如果支付接口仍然存在多次回调(例如 处理中->成功/失败),则不可进行 close()。
  • awaitClose() 用于在流关闭后(例如 VM 销毁、Flow 被取消等),释放 SDK 的 listener(例如 unregisterListener),防止资源泄漏。

在 ViewModel 里调用支付接口

经过改写后,在 VM 层就可以通过统一的方式调用支付,这样做的好处是,外部代码观察到的是同一的 Flow 接口,而底层具体实现可以无痛替换(例如改为微信支付、银联支付、苹果支付等)。

kotlin 复制代码
viewModelScope.launch {
    paymentSdk.pay("123456")
        .collect { result ->
            when (result) {
                is PaymentResult.Success -> println("支付成功")
                is PaymentResult.Failed -> println("支付失败")
            }
        }
}

2. 使用 ActivityComponent 引起的生命周期倒置问题

在唤起聚合支付 SDK 所提供的收银台过程中,其支付接口 pay(activity, orderInfo) 需要传入一个 Activity 类型的参数,这样设计的原因,是由于收银台会以类似 Dialog Fragment 的方式呈现在当前页面上,涉及到页面视图的引用,因此必须使用到 Activity 对象。

但是,这对我们的分层设计产生了一个不大不小的困扰,最初对于分层的设计如下:

  1. DataSource
  2. Repository
  3. ViewModel
  4. UI

其中 DataSource 位于最底层,提供支付 SDK、后端接口等的抽象。在上述分层设计里,除了最上层 UI,其他各层是不会接触到页面层级对象的,正是由于这种设计,才能够保持 ViewModel 独立于页面生命周期的特性,以便在页面销毁后重建时恢复状态。

然而,如果通过 Hilt 将 Activity 提供给 PaymentSdk,虽然可以简化支付接口的层层调用,使其只需要传递订单信息,但却打破了这种解耦关系。

错误写法,将 PaymentSdk 生命周期声明为页面级

kotlin 复制代码
@Module
@InstallIn(ActivityComponent::class)
abstract class PaymentModule {

    @Binds
    abstract fun bindPaymentSdkWrapper(
        impl: AlipaySdkWrapper
    ): PaymentSdkWrapper
}

上面这种写法会导致 Repository、DataSource 生命周期倒置的问题。Repository 是全局单例,而它持有了 DataSource,因此后者必须也是全局单例,从而将其声明为 ActivityComponent 是行不通的,除非也将 Repository 改写为同样的 ActivityComponent。但这样的话,会导致 Repository 跟随页面销毁重建,并没有必要这么做,在实现时还是要尽可能保持整洁和精简。

因此,最终采用的方案是,保持 AlipaySdk.pay() 接口的 Activity 参数,从 Activity-VieModel-Repository 中层层传递过来。

CourseDetailActivity.kt

kotlin 复制代码
btnPay.setOnClickListener {
    viewModel.pay(this)
}

3. ViewModel 对外暴露状态:成员变量 or 接口返回值

这也是一个值得探究的问题。

我们知道在传统的 LiveData 模式中,ViewModel 内部持有 LiveData 成员变量,Activity 通过订阅该对象,实现对数据流的监听。

在 Flow 问世以来,它逐渐替代掉了老旧的 LiveData,提供 StateFlow、SharedFlow 供页面监听,它的写法是与 LiveData 相似的。

但有一点区别尤其明显------ Flow 可以用作函数返回值,并且这也是一种非常典型的 Flow 用法。

1. UI State 成员变量模式

特点是 状态驱动 ,ViewModel 内部维护页面状态变量。注意到这里更底层的 Repository 实际上是返回的 Flow 对象,在 ViewModel 收集它时,用 collect 对内部状态进行更新。

ViewModel 层更新状态

kotlin 复制代码
@HiltViewModel
class PaymentViewModel @Inject constructor(
    private val repository: PaymentRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(PaymentUiState(idle = true))
    val uiState: StateFlow<PaymentUiState> = _uiState

    fun pay(activity: Activity, orderInfo: OrderInfo) {
        viewModelScope.launch {
            repository.startPay(activity, orderInfo) // ===> 下游 Repository 接口返回 Flow<PaymentResult> 对象
                .map { result ->
                    when (result) {
                        is PaymentResult.Success -> PaymentUiState(success = true, message = "支付成功")
                        is PaymentResult.Failure -> PaymentUiState(success = false, message = result.reason)
                        is PaymentResult.Cancelled -> PaymentUiState(success = false, message = "用户取消")
                    }
                }
                .collect { state ->
                    _uiState.value = state
                }
        }
    }
}

UI 层收集状态

kotlin 复制代码
lifecycleScope.launchWhenStarted {
    viewModel.uiState.collect { uiState ->
        when {
            uiState.idle -> showIdleUI()
            uiState.success -> showSuccessUI()
            else -> showErrorUI(uiState.message)
        }
    }
}

2. ViewModel 函数返回值式

特点是 流程驱动 ,Activity 调用 ViewModel 函数 -> 函数返回状态 -> Activity 收集函数返回的状态。由于 ViewModel 内部不对状态进行维护,每一次调用都生成独立的流,能够用于 多订单并行支付 的场景。

Activity 调用 vm.pay() 获得 StateFlow,进行收集并展示

kotlin 复制代码
@AndroidEntryPoint
class PaymentActivity : AppCompatActivity() {

    private val viewModel: PaymentViewModel by viewModels()

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

        val orderInfo = OrderInfo(id = "12345")

        lifecycleScope.launchWhenStarted {
            viewModel.pay(this@PaymentActivity, orderInfo).collect { uiState ->
                when {
                    uiState.idle -> showIdleUI()
                    uiState.success -> showSuccessUI()
                    else -> showErrorUI(uiState.message)
                }
            }
        }
    }
}

3. 我的选择

最终在两者之间,我选择了前者,即 UI State 成员变量模式。对于 Activity 来说,维护在 ViewModel 中的状态更适合描述和记录页面信息,在发生问题时,通过打印当前状态,也更容易进行定位排查。

4. 选择 Fake 实现 所处的层次

使用 Hilt 的一个重要优势,就是可以在开发阶段,在真实业务逻辑实现与 Fake 实现之间灵活切换。

  1. DataSource
  2. Repository
  3. ViewModel
  4. UI

回到我们的分层设计,在这个分层中,ViewModel 和 UI 作为终端呈现,没有为其设计虚假实现得必要。因此,我们对 Fake 对象的选择,要么是 FakeDataSource,要么是 FakeRepository。

在进行决策时,牢记这个原则:

越是底层的 Fake,其代码覆盖程度越广。

FakeRepository 与 FakeDataSourcee

对两种 Fake 方案的自测覆盖范围,对比阐述如下:

Fake 所在层次 Repository 层 DataSource 层
能测到 UI ✅ ✅
能测到 ViewModel ✅ ✅
能测到 Reopsitory ❌ ✅
能测到 DataSource ❌ ❌
开发复杂程度 低 中

可见两者最主要的区别是,使用 FakeDataSource 可以用来验证 Repository内部逻辑是否正确。后续在接入新的支付渠道时,测试逻辑能够保持一致性,测试范围的覆盖也更加到位。

最终我选择的是 FakeDataSource 来实现。

相关推荐
蒲公英内测分发1 小时前
智能投影仪参加海外展会,未上架的 Android 配套 App 怎么让客户限时试用?
android
ii_best1 小时前
按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)
android·ios·智能手机·自动化·ai编程·按键精灵
终端安全笔记1 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机
天远Date Lab1 小时前
零信任架构实战:基于天远全能消金报告(标准版)构建自动化骑手资信评估网关
运维·人工智能·架构·自动化
Shulex1 小时前
从问答到执行:跨境电商场景下抑制大模型幻觉与实现端到端任务闭环的工程架构实践
架构
乌萨达2 小时前
手机模拟器安卓怎么用?电脑安装应用、玩手游与账号同步指南
android·智能手机·电脑·雷电模拟器
美狐美颜SDK开放平台3 小时前
Android与iOS直播APP美颜有什么区别?视频美颜SDK开发详解
android·ios·音视频·视频美颜sdk
努力努力再努力wz3 小时前
【CUDA 入门系列】:Orin、GPU 并行执行模型与 CUDA 核心机制
架构
萧瑟余晖4 小时前
Dubbo 综合实战与性能调优详解
架构·dubbo
春猿火4 小时前
某市-2026【网安·论道】misc-afterimage_note-wp
android