一个库(以前称为 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 模块,并选择加入 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 的 @NativeCoroutinesState
注解,将你的 StateFlow
转换为 Swift 中的属性。
@NativeCoroutinesState
val travelEffect = _travelEffect.asStateFlow()
查看 KMP-NativeCoroutines README 以获取更多信息和安装说明。
或者,你可以在你的 iOS/Apple 源代码集中自己创建扩展属性
val TimeTravelViewModel.travelEffectValue: TravelEffect?
get() = travelEffect.value
像使用任何其他 AndroidX ViewModel 一样使用该 ViewModel
class TimeTravelFragment: Fragment(R.layout.fragment_time_travel) {
private val viewModel: TimeTravelViewModel by viewModels()
}
在你配置好你的 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 而不是 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,则你需要一些额外的逻辑。
首先,确保使用 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:)
。
在 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
函数。