为 KMP 设置 ViewModel

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

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

设置依赖项

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

[versions]
androidx-viewmodel = 2.9.3

[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 未导出,则只有在共享代码(来自 iosMaincommonMain 源集)中使用它们时,才能从二进制框架中获取这些 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 文档

使用 commonMainandroidMain 中的 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 生命周期会自动处理,并限定为 ComponentActivityFragmentNavBackStackEntry(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 生命周期

下一步是创建实现 ObservableObjectViewModelStoreOwner 接口(协议)的 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。在这种情况下,您需要使用一些替代的依赖注入框架,例如 Koinkotlin-injectMetroKodein。您可以在 klibs.io 上找到所有适用于 Kotlin Multiplatform 的依赖注入框架。

在 SwiftUI 中观察 Flow

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