為 KMP 設定 ViewModel

AndroidX ViewModel 就像是橋樑,在共用商業邏輯和 UI 元件之間建立明確的合約。這種模式有助於確保各平台的資料一致,同時可針對每個平台的不同外觀自訂 UI。您可以繼續使用 Android 的 Jetpack Compose 和 iOS 的 SwiftUI 開發 UI。

如要進一步瞭解使用 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 類別的形式提供。如要進一步瞭解如何匯出依附元件,請參閱「將依附元件匯出至二進位檔」。

如要改善體驗,您可以在定義 iOS 二進位架構的 build.gradle.kts 檔案中使用 export 設定,將 ViewModel 依附元件匯出至二進位架構,這樣就能直接從 Swift 程式碼存取 ViewModel API,就像從 Kotlin 程式碼存取一樣:

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,而電腦預設可能不支援此屬性。如要確保其正常運作,請在專案中新增 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 或 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()

接著,在 UI 程式碼中,您可以照常擷取 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 生命週期

下一步是建立 IosViewModelStoreOwner,實作 ObservableObject 和 ViewModelStoreOwner 介面 (通訊協定)。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。在這種情況下,您需要使用其他 DI 架構,例如 Koin、kotlin-inject、Metro 或 Kodein。您可以在 klibs.io 找到所有與 Kotlin Multiplatform 搭配使用的 DI 架構。

在 SwiftUI 中觀察流程

SwiftUI 不直接支援觀察協同程式 Flow。不過,您可以使用 KMP-NativeCoroutines 或 SKIE 程式庫來啟用這項功能。