为 KMP 设置 ViewModel

AndroidX ViewModel 充当桥梁,在共享业务逻辑与界面组件之间建立明确的合约。此模式有助于确保数据在各个平台之间保持一致,同时还可让界面根据每个平台的独特外观进行自定义。您可以继续在 Android 上使用 Jetpack Compose,在 iOS 上使用 SwiftUI 开发界面。

如需详细了解使用 ViewModel 的好处以及所有功能,请参阅 ViewModel 的主要文档。

设置依赖项

如需在项目中设置 KMP ViewModel,请在 libs.versions.toml 文件中定义依赖项:

[versions]
androidx-viewmodel = 2.11.0

[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }

然后,将该制品添加到 KMP 模块的 build.gradle.kts 文件中,并将依赖项声明为 api,因为此依赖项将导出到二进制框架:

// You need the "api" dependency declaration here if you want better access to the classes from Swift code.
commonMain.dependencies {
  api(libs.androidx.lifecycle.viewmodel)
}

导出 ViewModel API 以便从 Swift 进行访问

默认情况下,您添加到代码库中的任何库都不会自动导出到二进制框架。如果 API 未导出,则只有在共享代码(来自 iosMain 或 commonMain 源代码集)中使用它们时,才能从二进制框架中获取这些 API。在这种情况下,API 将包含软件包前缀,例如 ViewModel 类将作为 Lifecycle_viewmodelViewModel 类提供。如需详细了解如何导出依赖项,请参阅导出到二进制文件的依赖项。

为了改善体验,您可以使用 build.gradle.kts 文件中定义 iOS 二进制框架的 export 设置,将 ViewModel 依赖项导出到二进制框架,这样就可以直接从 Swift 代码(与从 Kotlin 代码一样)访问 ViewModel API:

listOf(
  iosX64(),
  iosArm64(),
  iosSimulatorArm64(),
).forEach {
  it.binaries.framework {
    // Add this line to all the targets you want to export this dependency
    export(libs.androidx.lifecycle.viewmodel)
    baseName = "shared"
  }
}

(可选)在 JVM 桌面设备上使用 viewModelScope

在 ViewModel 中运行协程时,viewModelScope 属性与 Dispatchers.Main.immediate 相关联,而 Dispatchers.Main.immediate 在桌面设备上可能默认不可用。为使其正常运行,请将 kotlinx-coroutines-swing 依赖项添加到您的项目中:

// Optional if you use JVM Desktop
desktopMain.dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:[KotlinX Coroutines version]")
}

如需了解详情,请参阅 Dispatchers.Main 文档。

使用 commonMain 或 androidMain 中的 ViewModel

在共享 commonMain 中使用 ViewModel 类,以及从 androidMain sourceSet 中使用 ViewModel 类,都没有具体要求。唯一需要考虑的是,您无法使用任何特定于平台的 API,并且需要对它们进行抽象化处理。例如,如果您使用 Android Application 作为 ViewModel 构造函数参数,则需要通过抽象化此 API 来迁移。

如需详细了解如何使用平台专用代码,请参阅 Kotlin Multiplatform 中的平台专用代码。

例如,以下代码段中有一个 ViewModel 类及其工厂,定义在 commonMain 中:

// commonMain/MainViewModel.kt

class MainViewModel(
    private val repository: DataRepository,
) : ViewModel() { /* some logic */ }

// ViewModelFactory that retrieves the data repository for your app.
val mainViewModelFactory = viewModelFactory {
    initializer {
        MainViewModel(repository = getDataRepository())
    }
}

fun getDataRepository(): DataRepository = DataRepository()

然后,您可以在界面代码中照常检索 ViewModel:

// androidApp/ui/MainScreen.kt

@Composable
fun MainScreen(
    viewModel: MainViewModel = viewModel(
        factory = mainViewModelFactory,
    ),
) {
// observe the viewModel state
}

从 SwiftUI 使用 ViewModel

在 Android 上,ViewModel 生命周期会自动处理,并限定为 ComponentActivity、Fragment、NavBackStackEntry(Navigation 2)或 rememberViewModelStoreNavEntryDecorator(Navigation 3)。不过,iOS 上的 SwiftUI 没有 AndroidX ViewModel 的内置等效项。

如需与 SwiftUI 应用共享 ViewModel,您需要添加一些设置代码。

创建有助于使用泛型的函数

实例化泛型 ViewModel 实例时,会使用 Android 上的类引用反射功能。由于 Objective-C 泛型不支持 Kotlin 或 Swift 的所有功能,因此您无法直接从 Swift 中检索泛型类型的 ViewModel。

为了解决此问题,您可以创建一个辅助函数,该函数将使用 ObjCClass 而不是泛型类型,然后使用 getOriginalKotlinClass 检索要实例化的 ViewModel 类:

// iosMain/ViewModelResolver.ios.kt

/**
 *   This function allows retrieving any ViewModel from Swift Code with generics. We only get
 *   [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code
 *   doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin.
 */
@BetaInteropApi
@Throws(IllegalArgumentException::class)
fun ViewModelStore.resolveViewModel(
    modelClass: ObjCClass,
    factory: ViewModelProvider.Factory,
    key: String?,
    extras: CreationExtras? = null,
): ViewModel {
    @Suppress("UNCHECKED_CAST")
    val vmClass = getOriginalKotlinClass(modelClass) as? KClass<ViewModel>
    require(vmClass != null) { "The modelClass parameter must be a ViewModel type." }

    val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty)
    return key?.let { provider[key, vmClass] } ?: provider[vmClass]
}

然后,当您想从 Swift 中调用该函数时,可以编写一个 T : ViewModel 类型的泛型函数,并使用 T.self,该函数可以将 ObjCClass 传递给 resolveViewModel 函数。

将 ViewModel 范围连接到 SwiftUI 生命周期

下一步是创建实现 ObservableObject 和 ViewModelStoreOwner 接口(协议)的 IosViewModelStoreOwner。之所以使用 ObservableObject,是为了能够在 SwiftUI 代码中将此类用作 @StateObject:

// iosApp/IosViewModelStoreOwner.swift

class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner {

    let viewModelStore = ViewModelStore()

    /// This function allows retrieving the androidx ViewModel from the store.
    /// It uses the utilify function to pass the generic type T to shared code
    func viewModel<T: ViewModel>(
        key: String? = nil,
        factory: ViewModelProviderFactory,
        extras: CreationExtras? = nil
    ) -> T {
        do {
            return try viewModelStore.resolveViewModel(
                modelClass: T.self,
                factory: factory,
                key: key,
                extras: extras
            ) as! T
        } catch {
            fatalError("Failed to create ViewModel of type \(T.self)")
        }
    }

    /// This is called when this class is used as a `@StateObject`
    deinit {
        viewModelStore.clear()
    }
}

此所有者允许检索多种 ViewModel 类型,与 Android 类似。 当使用 IosViewModelStoreOwner 的屏幕被取消初始化并调用 deinit 时,这些 ViewModel 的生命周期会被清除。如需详细了解反初始化,请参阅官方文档。

此时,您只需在 SwiftUI 视图中将 IosViewModelStoreOwner 实例化为 @StateObject,然后调用 viewModel 函数来检索 ViewModel:

// iosApp/ContentView.swift

struct ContentView: View {

    /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen.
    @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner()

    var body: some View {
        /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`.
        /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection
        /// and proper initialization of the ViewModel with its required `AppContainer`.
        let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel(
            factory: MainViewModelKt.mainViewModelFactory
        )
        // ...
        // .. the rest of the SwiftUI code
    }
}

在 Kotlin Multiplatform 中不可用

Android 上可用的某些 API 在 Kotlin Multiplatform 中不可用。

与 Hilt 集成

由于 Hilt 不适用于 Kotlin Multiplatform 项目,因此您无法在 commonMain sourceSet 中直接使用带有 @HiltViewModel 注解的 ViewModel。在这种情况下,您需要使用一些替代的依赖注入框架,例如 Koin、kotlin-inject、Metro 或 Kodein。您可以在 klibs.io 上找到所有适用于 Kotlin Multiplatform 的依赖注入框架。

在 SwiftUI 中观察 Flow

SwiftUI 不直接支持观测协程 Flow。不过,您可以使用 KMP-NativeCoroutines 或 SKIE 库来启用此功能。