轻糖的 KMP 渐进式迁移实践(三):KMPObservableViewModel 直连,iOS 本地 ViewModel 全量退休

轻糖的 KMP 渐进式迁移实践(三):KMPObservableViewModel 直连,iOS 本地 ViewModel 全量退休

💡 原文中文,约16900字,阅读约需41分钟。
📝

内容提要

本文介绍轻糖App的KMP渐进式迁移第三阶段:用KMPObservableViewModel直连SwiftUI,删除iOS本地StateHolder。通过配置插件和注解,SwiftUI直接观察KMP ViewModel,删除约2242行代码。迁移分两批完成,平台能力通过协议注入,实现双端共享业务逻辑,新功能默认KMP-first开发。

🔎

延伸解读

StateHolder 的“数量税”与生命周期悬空

文章指出,虽然单个 StateHolder 桥接代码不到 50 行,但 23 个 ViewModel 就需要 23 份重复样板,且 KMP 侧 ViewModel 在 iOS 上缺乏 ViewModelStore,viewModelScope 协程无法自动取消,依赖手动 deinit cancel,漏写即泄漏。这揭示了桥接层虽薄但存在结构性成本,是推动进一步迁移的关键动因。

KMPObservableViewModel 的配置要点

迁移依赖 KMP-ObservableViewModel 和 KMP-NativeCoroutines 插件。配置需四步:添加版本、挂插件并导出依赖、集成 SPM 包、扩展协议。关键改动是 KMP 侧必须使用 observableviewmodel 的 MutableStateFlow(viewModelScope, initial) 并加 @NativeCoroutinesState 注解,否则 SwiftUI 无法收到状态更新。这提醒开发者注意库的特定用法。

平台能力边界:协议注入与桥接友好字段

文章强调平台 SDK 不进入 KMP,而是通过协议反向注入。例如 AuthSignInHelper 只负责产出 idToken,HealthKitBridgeDelegate 由 Swift 实现 KMP 协议。同时,UiState 中为 Swift 消费的值提供桥接友好字段(如 epochMillis、布尔旁路),避免类型转换。这些模式保证了双端行为一致,是迁移成功的关键实践。

迁移收益与潜在风险

迁移删除了约 2242 行 Swift 代码,实现单一事实来源和行为一致性,新功能默认 KMP-first。但文章也记录了踩坑:Swift 6 并发需 @preconcurrency、init 自动刷新导致重复请求、companion 静态 map 共享数据问题。这些风险提示开发者,迁移后仍需注意生命周期管理和并发安全,避免引入新问题。

Q&A

轻糖App在KMP迁移第三阶段中,为什么决定删除iOS本地的StateHolder?

因为StateHolder存在样板重复、生命周期悬空和双端行为不对称等问题。每个ViewModel都需要写重复的@Published、for await和deinit cancel代码,且KMP侧ViewModel在iOS上没有ViewModelStore,协程生命周期无法自动管理,容易泄漏。此外,iOS的for await订阅无法像Android那样感知生命周期。因此,引入KMP-ObservableViewModel后,SwiftUI可以直接观察KMP ViewModel,从而删除StateHolder。

KMP-ObservableViewModel和KMP-NativeCoroutines在迁移中分别起什么作用?

KMP-ObservableViewModel是androidx ViewModel的KMP移植版,使viewModelScope在iOS上可用,并提供MutableStateFlow(viewModelScope, initialValue)构造,将状态与ViewModel生命周期绑定。KMP-NativeCoroutines是Gradle编译器插件,通过@NativeCoroutinesState注解为StateFlow生成Swift可观察桥接,使SwiftUI的property wrapper可以直接订阅,无需手动for await循环。

在KMP侧,ViewModel改造需要做哪三处关键改动?

三处改动为:1. 继承类从androidx.lifecycle.ViewModel换成com.rickclephas.kmp.observableviewmodel.ViewModel;2. 状态容器换成MutableStateFlow(viewModelScope, initialValue),不能用标准kotlinx.coroutines.flow.MutableStateFlow;3. StateFlow属性加@NativeCoroutinesState注解,让编译器插件生成Swift侧可观察桥接。

SwiftUI视图如何直接观察KMP ViewModel?

SwiftUI视图使用@StateViewModel或@ObservedViewModel property wrapper替代@StateObject/@ObservedObject,直接持有KMP ViewModel实例。例如:@StateViewModel private var viewModel = Shared.FoodDetailViewModel()。这样SwiftUI会自动订阅@NativeCoroutinesState生成的可观察属性,视图销毁时自动取消订阅。

迁移分两批进行,第一批选择了哪些ViewModel试点?

第一批选择了5个边界清晰的ViewModel试点:AICreditViewModel、FoodDetailViewModel、AddMedicationRecordViewModel、EditBloodSugarRecordViewModel、EditExerciseRecordViewModel。

迁移后iOS工程中还有哪些@StateObject?为什么它们没有被删除?

迁移后iOS工程只剩2处@StateObject:蓝牙血糖仪适配器GlucoseDeviceManager和ToastManager。它们不是业务ViewModel,而是平台能力与UI工具,属于Adapter层的合理存在,因此保留。

在平台能力边界上,文章提出了哪三个关键模式?

三个模式为:1. AuthSignInHelper:平台SDK只负责产出idToken,业务登录逻辑在共享的AuthViewModel中;2. HealthKitBridgeDelegate:Swift实现KMP协议,通过Koin注入回共享层,HealthKit权限和数据读取留在平台层;3. DTO单轨+桥接友好字段:UiState直接持有DTO,并为Swift消费提供epochMillis、布尔旁路等友好字段,避免转换。

迁移过程中遇到了哪些主要坑?如何解决?

主要坑有三个:1. Swift 6严格并发导致RegionIsolation报错,解决方法是使用@preconcurrency import;2. ViewModel在init中自动刷新导致每次重建都发网络请求,解决方法是移除init自动刷新,改为视图层.task驱动,并加30秒冷却;3. companion静态map导致多视图实例共享过期数据,解决方法是改为实例字段。

迁移完成后,新功能开发流程有什么变化?

新功能默认KMP-first开发。以统计报告功能为例,开发路径为:写PRD,在shared/中写领域模型和UseCase,写KMP ViewModel(直接按新范式),然后iOS和Android各自只写视图层。共享代码无需讨论,双端视图直接消费ViewModel。

这次迁移带来了哪些收益?

收益包括:单一事实来源(业务逻辑只有一份实现,避免双端漂移)、行为一致性(双端页面行为完全一致)、开发效率提升(功能逻辑先写在KMP,双端同时可用)、代码量减少(删除约2242行Swift ViewModel代码,ViewModels目录消失)。

🏷️

标签

➡️

继续阅读