AndroidX ViewModel 可做為橋樑,在共用的商業邏輯和 UI 元件之間建立明確的合約。這個模式有助於確保各平台資料一致,同時讓 UI 能夠根據各平台的獨特外觀進行自訂。您可以在 Android 上使用 Jetpack Compose,在 iOS 上使用 SwiftUI,繼續開發 UI。
如要進一步瞭解使用 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,只有在共用程式碼 (來自 iosMain
或 commonMain
來源集) 中使用 API 時,才能透過二進位架構存取 API。在這種情況下,API 會包含套件前置字元,例如 ViewModel
類別會以 Lifecycle_viewmodelViewModel
類別的形式提供。如要進一步瞭解如何匯出依附元件,請參閱將依附元件匯出至二進位檔。
如要提升體驗,您可以使用 build.gradle.kts
檔案中定義 iOS 二進位架構的 export
設定,將 ViewModel 依附元件匯出至二進位架構,讓 ViewModel API 可直接從 Swift 程式碼存取,就像從 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 Desktop 上使用 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
中使用 ViewModel 類別沒有特定需求,androidMain
sourceSet 也是如此。唯一需要考慮的是,您無法使用任何平台專屬的 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,您需要加入一些設定程式碼。
建立函式來協助處理泛型
在 Android 上,建立泛型 ViewModel 例項時,會使用類別參照反射功能。由於 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
Kotlin Multiplatform 不支援部分 Android API。
與 Hilt 整合
由於 Hilt 不適用於 Kotlin Multiplatform 專案,因此您無法在 commonMain
sourceSet 中,直接使用附有 @HiltViewModel
註解的 ViewModel。在這種情況下,您需要使用其他 DI 架構,例如 Koin、kotlin-inject、Metro 或 Kodein。您可以在 klibs.io 找到所有適用於 Kotlin Multiplatform 的 DI 架構。
在 SwiftUI 中觀察 Flow
系統不直接支援在 SwiftUI 中觀察協同程式流程。不過,您可以使用 KMP-NativeCoroutines 或 SKIE 程式庫啟用這項功能。