3. 跨平台 ViewModel 设计:expect/actual 模式、工厂方法、生命周期感知、与 Compose 绑定
跨平台 ViewModel 是 KMP 状态共享架构中的核心协调层。本章聚焦如何通过 Kotlin 的 expect/actual 机制,在共享模块中定义抽象、在平台模块中注入具体实现,从而构建一个既具备生命周期感知能力又能与 Jetpack Compose / Compose Multiplatform 直接绑定的 ViewModel 体系。
3.1 架构边界与职责划分
在 KMP 项目中,ViewModel 处于 UI 层与数据层之间。其职责边界需要严格遵守下列原则:
| 角色 | 职责 | 越界风险 |
|---|---|---|
| Shared ViewModel | 管理 UI 状态(StateFlow)、处理用户意图、调度业务逻辑到 UseCase / Repository | 直接持有平台依赖(如 Context、View)、执行 UI 渲染 |
| Platform ViewModel | 提供生命周期所有者、处理屏幕旋转恢复、绑定 Compose 生命周期 | 包含业务判断、跨模块直接调用数据层 |
| Expect/Actual 桥接层 | 定义跨平台接口契约、封装平台工厂方法、注入生命周期实例 | 暴露平台细节到共享层、破坏编译隔离 |
关键权衡:完全共享 ViewModel 会丧失平台生命周期能力;完全平台化 ViewModel 会导致逻辑不可复用。expect/actual 提供了一条介于两者之间的“中间路径”——在共享模块中描述“要什么”,在平台模块中描述“如何给”。
3.2 expect/actual 工厂方法设计
工厂方法用于隔离 ViewModel 的创建过程,使得共享层无需直接引用平台构造函数。
以下展示在共享模块中定义 expectation:
// shared/src/commonMain/kotlin/com/example/app/viewmodel/PlatformViewModel.kt
expect class PlatformViewModelFactory {
fun create(clazz: Class): T
}
expect open class PlatformViewModel {
open fun onCleared()
}
在 Android 平台 actual 实现中,使用 AndroidX ViewModelProvider 的工厂模式:
// shared/src/androidMain/kotlin/com/example/app/viewmodel/PlatformViewModel.kt
actual open class PlatformViewModel : androidx.lifecycle.ViewModel() {
override fun onCleared() { super.onCleared() }
}
actual class PlatformViewModelFactory(private val application: Application) {
actual fun create(clazz: Class): T {
val provider = ViewModelProvider(
/* 需要 activity 或 fragment */,
ViewModelProvider.AndroidViewModelFactory(application)
)
return provider.get(clazz as Class) as T
}
}
风险点:平台依赖泄漏 上述 Android actual 中隐含了
Application 依赖。建议将 Application 通过 CompositionLocal 或 Service Locator 间接注入,而非在 actual 工厂中硬编码。
3.3 生命周期感知的实现策略
生命周期感知是 ViewModel 的核心能力。在 KMP 场景下,不同平台的生命周期对象不同,需要通过 expect/actual 统一抽象:
| 平台 | 生命周期对象 | 绑定方式 |
|---|---|---|
| Android | Lifecycle(AndroidX) | ViewModelProvider + Lazy |
| iOS | NSObject / UIScene(需手动管理) | Kotlin/Native 弱引用 + 显式注册 |
| Desktop (JVM) | 无内置生命周期 | Compose Window + DisposableEffect |
为了统一处理,在共享层定义如下生命周期范围接口:
// shared/src/commonMain/kotlin/com/example/app/lifecycle/LifecycleOwner.kt
interface LifecycleOwner {
val lifecycle: Lifecycle
}
enum class LifecycleEvent {
ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY
}
expect class Lifecycle {
fun observe(event: LifecycleEvent, action: () -> Unit)
fun removeObservers()
}
实际在 Android 上桥接到 LifecycleObserver,在 iOS 上使用 KVO 或 NotificationCenter,在 Desktop 上通过 Composition 生命周期手动触发。
3.4 与 Compose 绑定的数据流控制
ViewModel 的状态必须通过 StateFlow 暴露给 Compose,并在 Composable 层级中正确收集:
- 数据流方向:ViewModel → UI State (StateFlow) → Composable (collectAsState)
- 控制流方向:Composable (用户事件) → ViewModel (方法调用) → UseCase / Repository
- 信任链路:状态由 ViewModel 持有,Composable 只读不写;业务逻辑由 ViewModel 发起,Composable 不直接调用数据层
核心绑定代码(共享模块):
// shared/src/commonMain/kotlin/com/example/app/ui/MainScreen.kt
@Composable
fun MainScreen(viewModel: MainViewModel) {
val uiState by viewModel.uiState.collectAsState()
// 渲染 UI
Button(onClick = { viewModel.onButtonClicked() }) {
Text("点击")
}
}
在 Android 端,通过 Compose Navigation 或传统 Activity 传入 ViewModel:
// androidApp
@Composable
fun AndroidMainScreen() {
val viewModel: MainViewModel = viewModel() // 依赖注入
MainScreen(viewModel = viewModel)
}
在 iOS 端,通过 Swift 桥接调用 KMP ViewModel 实例,并手动管理观察者。
3.5 架构总图:expect/actual ViewModel 全链路
3.6 验收标准
- 编译隔离验证:共享模块不应引用任何平台特定 API(android.content、UIKit、java.awt)。仅通过 expect/actual 桥接。
- 生命周期测试:在 Android 上旋转屏幕后 ViewModel 状态不丢失;iOS 上进入后台/前台时观察者正确注册/移除。
- 状态一致性:多平台使用同一份 ViewModel 业务逻辑代码运行后,UI State 的初始值、触发流程、收拢结果完全一致。
- 工厂方法可注入:PlatformViewModelFactory 实例化时不依赖全局单例,可通过构造函数或 DI 框架传递。
- Compose 绑定无崩溃:所有 Composable 函数在组合/重组/销毁期间不应因 ViewModel 引用泄漏或生命周期不匹配而崩溃。
落地建议:从 Android 平台开始实施 expect/actual ViewModel 方案,因为 AndroidX ViewModel 体系最成熟。随后再逐个覆盖 iOS 和 Desktop。初期避免在共享层暴露复杂生命周期接口,优先保证
StateFlow + collectAsState 双向绑定可用。