KMP-ObservableViewModel

一个库(以前称为 KMM-ViewModel),允许你将 AndroidX/Kotlin ViewModels 与 SwiftUI 一起使用。

兼容性

你可以在任何 KMP 项目中使用这个库,但并非所有目标都支持 AndroidX 和/或 SwiftUI 互操作。

目标平台 支持 AndroidX SwiftUI
Android -
JVM -
iOS
macOS
tvOS -
watchOS -
linuxX64 -
linuxArm64 - -
mingwX64 - -
JS - -
Wasm - -

该库的最新版本使用 Kotlin 版本 2.1.10
也提供用于旧版本和/或预览版 Kotlin 的兼容版本

版本 版本后缀 Kotlin 协程 (Coroutines) AndroidX Lifecycle
最新版本 -kotlin-2.1.20-Beta2 2.1.20-Beta2 1.10.1 2.8.7
最新版本 无后缀 2.1.10 1.10.1 2.8.7
1.0.0-BETA-8 无后缀 2.1.0 1.9.0 2.8.4
1.0.0-BETA-7 无后缀 2.0.21 1.9.0 2.8.4
1.0.0-BETA-6 无后缀 2.0.20 1.9.0 2.8.4
1.0.0-BETA-4 无后缀 2.0.10 1.8.1 2.8.4
1.0.0-BETA-3 无后缀 2.0.0 1.8.1 2.8.0
1.0.0-BETA-2 无后缀 1.9.24 1.8.1 2.8.0

Kotlin

将该库添加到你的共享 Kotlin 模块,并选择加入 ExperimentalForeignApi

kotlin {
    sourceSets {
        all {
            languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
        }
        commonMain {
            dependencies {
                api("com.rickclephas.kmp:kmp-observableviewmodel-core:1.0.0-BETA-9")
            }
        }
    }
}

并创建你的 ViewModels

import com.rickclephas.kmp.observableviewmodel.ViewModel
import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
import com.rickclephas.kmp.observableviewmodel.stateIn

open class TimeTravelViewModel: ViewModel() {

    private val clockTime = Clock.time

    /**
     * A [StateFlow] that emits the actual time.
     */
    val actualTime = clockTime.map { formatTime(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)
    /**
     * A [StateFlow] that emits the applied [TravelEffect].
     */
    val travelEffect = _travelEffect.asStateFlow()
}

你可能注意到它与 AndroidX ViewModel 没有太大区别。
我们显然使用的是不同的 ViewModel 超类

- import androidx.lifecycle.ViewModel
+ import com.rickclephas.kmp.observableviewmodel.ViewModel

open class TimeTravelViewModel: ViewModel() {

但除此之外,只有两个小的区别。
第一个是 stateIn 的不同导入

- import kotlinx.coroutines.flow.stateIn
+ import com.rickclephas.kmp.observableviewmodel.stateIn

        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

第二个是不同的 MutableStateFlow 构造函数

- import kotlinx.coroutines.flow.MutableStateFlow
+ import com.rickclephas.kmp.observableviewmodel.MutableStateFlow

-    private val _travelEffect = MutableStateFlow<TravelEffect?>(null)
+    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)

这些微小的差异将确保状态更改传播到 SwiftUI。

注意

viewModelScope 是实际 CoroutineScope 的包装器,可以通过 ViewModelScope.coroutineScope 属性访问。

KMP-NativeCoroutines

我强烈建议你使用来自 KMP-NativeCoroutines@NativeCoroutinesState 注解,将你的 StateFlow 转换为 Swift 中的属性。

@NativeCoroutinesState
val travelEffect = _travelEffect.asStateFlow()

查看 KMP-NativeCoroutines README 以获取更多信息和安装说明。

替代方案

或者,你可以在你的 iOS/Apple 源代码集中自己创建扩展属性

val TimeTravelViewModel.travelEffectValue: TravelEffect?
    get() = travelEffect.value

Android

像使用任何其他 AndroidX ViewModel 一样使用该 ViewModel

class TimeTravelFragment: Fragment(R.layout.fragment_time_travel) {
    private val viewModel: TimeTravelViewModel by viewModels()
}

Swift

在你配置好你的 shared Kotlin 模块并创建了一个 ViewModel 之后,现在是配置你的 Swift 项目的时候了。
首先,将 Swift 包添加到你的 Package.swift 文件中

dependencies: [
    .package(url: "https://github.com/rickclephas/KMP-ObservableViewModel.git", from: "1.0.0-BETA-9")
]

或者,在 Xcode 中通过 File > Add Packages... 并提供 URL: https://github.com/rickclephas/KMP-ObservableViewModel.git 来添加它。

CocoaPods

如果你喜欢,你也可以使用 CocoaPods 而不是 SPM

pod 'KMPObservableViewModelSwiftUI', '1.0.0-BETA-9'

创建一个包含以下内容的 KMPObservableViewModel.swift 文件

import KMPObservableViewModelCore
import shared // This should be your shared KMP module

extension Kmp_observableviewmodel_coreViewModel: ViewModel { }

之后,你可以像使用 ObservableObject 一样使用你的 ViewModel。
只需使用 ViewModel 特定的属性包装器和函数

ObservableObject ViewModel
@StateObject @StateViewModel
@ObservedObject @ObservedViewModel
@EnvironmentObject @EnvironmentViewModel
environmentObject(_:) environmentViewModel(_:)

例如,要将 TimeTravelViewModel 用作 StateObject

import SwiftUI
import KMPObservableViewModelSwiftUI
import shared // This should be your shared KMP module

struct ContentView: View {
    @StateViewModel var viewModel = TimeTravelViewModel()
}

也可以在 Swift 中继承你的 ViewModel

import Combine
import shared // This should be your shared KMP module

class TimeTravelViewModel: shared.TimeTravelViewModel {
    @Published var isResetDisabled: Bool = false
}

子 ViewModel

如果你的 ViewModel 公开了子 ViewModel,则你需要一些额外的逻辑。

首先,确保使用 NativeCoroutinesRefinedState 注解而不是 NativeCoroutinesState 注解

class MyParentViewModel: ViewModel() {
    @NativeCoroutinesRefinedState
    val myChildViewModel: StateFlow<MyChildViewModel?> = MutableStateFlow(null)
}

之后,你应该使用 childViewModel(at:) 函数创建一个 Swift 扩展属性

extension MyParentViewModel {
    var myChildViewModel: MyChildViewModel? {
        childViewModel(at: \.__myChildViewModel)
    }
}

这将防止你的 Swift ViewModels 过早地被释放。

注意

对于包含 ViewModels 的列表、集合和字典,存在 childViewModels(at:)

可取消的 ViewModel

在 Swift 中继承你的 Kotlin ViewModel 时,你可能会遇到一些关于这些 ViewModels 清理方式的问题。

一个这样的问题示例是,当你使用 Combine 发布者通过 KMP-NativeCoroutines 观察 Flow 时

import Combine
import KMPNativeCoroutinesCombine
import shared // This should be your shared KMP module

class TimeTravelViewModel: shared.TimeTravelViewModel {

    private var cancellables = Set<AnyCancellable>()

    override init() {
        super.init()
        createPublisher(for: currentTimeFlow)
            .assertNoFailure()
            .sink { time in print("It's \(time)") }
            .store(in: &cancellables)
    }
}

由于 currentTimeFlow 是一个 StateFlow,我们永远不希望它失败,这就是为什么我们使用 assertNoFailure。但是,在这种情况下,你会注意到发布者将以 JobCancellationException 失败。

这里的问题是,在 TimeTravelViewModel 被释放之前,它已经被清理了。这意味着 viewModelScope 被取消,并且调用了 onCleared。这导致 Combine 发布者比底层 StateFlow 集合的生命周期更长。

为了解决此类问题,你应该让你的 Swift ViewModel 符合 Cancellable 协议,并在 cancel 函数中执行所需的清理

class TimeTravelViewModel: shared.TimeTravelViewModel, Cancellable {
    func cancel() {
        cancellables = []
    }
}

KMP-ObservableViewModel 将确保在 ViewModel 被清理之前调用 cancel 函数。