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 全链路

跨平台 ViewModel Design with expect/actual 跨平台 ViewModel 架构:expect/actual 全链路 共享模块 (commonMain) expect PlatformViewModel expect PlatformFactory expect Lifecycle 状态共享:StateFlow<UiState> + collectAsState() 平台模块 (androidMain / iosMain / desktopMain) actual ViewModel (AndroidX) LifecycleOwner → observe actual ViewModel (K/N) NSObject + KVO / 显式注册 actual ViewModel (JVM) DisposableEffect 管理 工厂方法:PlatformViewModelFactory.create() → 实例化 + 生命周期绑定 + 依赖注入 Composable 层:MainScreen(viewModel) → collectAsState → 渲染

3.6 验收标准

  1. 编译隔离验证:共享模块不应引用任何平台特定 API(android.content、UIKit、java.awt)。仅通过 expect/actual 桥接。
  2. 生命周期测试:在 Android 上旋转屏幕后 ViewModel 状态不丢失;iOS 上进入后台/前台时观察者正确注册/移除。
  3. 状态一致性:多平台使用同一份 ViewModel 业务逻辑代码运行后,UI State 的初始值、触发流程、收拢结果完全一致。
  4. 工厂方法可注入:PlatformViewModelFactory 实例化时不依赖全局单例,可通过构造函数或 DI 框架传递。
  5. Compose 绑定无崩溃:所有 Composable 函数在组合/重组/销毁期间不应因 ViewModel 引用泄漏或生命周期不匹配而崩溃。
落地建议:从 Android 平台开始实施 expect/actual ViewModel 方案,因为 AndroidX ViewModel 体系最成熟。随后再逐个覆盖 iOS 和 Desktop。初期避免在共享层暴露复杂生命周期接口,优先保证 StateFlow + collectAsState 双向绑定可用。