多平台 ViewModel
Android ViewModel 允許您將應用程式的商務邏輯與 UI 元件連接。 透過 Compose Multiplatform,您也可以在通用程式碼中使用 ViewModel。
此頁面將引導您在多平台專案中設定並使用 ViewModel:
- 設定相依性。
- 在通用程式碼中使用 ViewModel。
- 將 ViewModel 限定在導覽目標的作用域內。
- 使用 Koin 或 Metro 注入相依性。
- 選擇要共用多少 ViewModel 和 UI 程式碼: 從完全共用的方式到僅共用存儲庫或資料層。
設定相依性
若要跨平台共用 ViewModel 和 UI:
在 Gradle 版本目錄檔案中定義相依性:
toml[versions] androidx-viewmodel = "2.10.0" [libraries] androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-viewmodel" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-viewmodel" }您可以在我們的新功能中追蹤多平台 ViewModel 實作的變更, 或在 Compose Multiplatform 變更記錄中關注早期體驗計劃版本。
在 KMP 模組的
build.gradle.kts檔案中,將以下相依性新增至commonMain原始碼集:kotlinkotlin { // ... sourceSets { // ... commonMain.dependencies { implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.navigation3) } // ... } }
相依性可能會根據您的程式碼共用方式而有所不同。詳情請參閱 undefined。
如果您有桌面平台目標,請同時新增 kotlinx-coroutines-swing 相依性。 在 ViewModel 中執行協同程式時,ViewModel.viewModelScope 會與 Dispatchers.Main.immediate 繫結, 而後者在預設情況下可能無法在桌面平台上使用。Kotlinx Coroutines Swing 程式庫可讓 ViewModel 協同程式與 Compose Multiplatform 正常運作。
在 Gradle 版本目錄中:
toml[versions] kotlinx-coroutines = "1.10.2" [libraries] kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }在
build.gradle.kts檔案中:kotlinkotlin { // ... sourceSets { // ... jvmMain.dependencies { implementation(libs.kotlinx.coroutines.swing) } // ... } }詳情請參閱
Dispatchers.Main文件。
在通用程式碼中使用 ViewModel
Compose Multiplatform 提供了一個通用的 ViewModelStoreOwner 實作,因此在通用程式碼中使用 ViewModel 類別與 Android 最佳實務並無太大差異。
然而,在非 JVM 平台上存在一個重要的差異,即無法使用型別反射來具現化物件。 您不能在通用程式碼中呼叫不含參數的 viewModel() 函式。 每次建立 ViewModel 執行個體時,您至少需要提供一個初始設定式作為引數。
如果僅提供初始設定式,Compose Multiplatform 會在底層建立預設工廠(類)。 但是,您可以實作自己的工廠(類)並呼叫更明確版本的通用 viewModel() 函式,就像使用 Jetpack Compose 一樣。
讓我們定義一個 ViewModel 並將其連接到可組合項:
定義一個簡單的
OrderViewModel類別來管理 UI 狀態,包括訂購項目的數量和價格:kotlindata class OrderUiState(val quantity: Int = 0, val price: String = "$0.00") class OrderViewModel : ViewModel() { val uiState: StateFlow<OrderUiState> field = MutableStateFlow(OrderUiState()) fun setQuantity(n: Int) { field.update { it.copy(quantity = n, price = "${n * 2}.00") } } }此範例使用明確支援欄位, 該功能在 Kotlin 2.4.0-RC 中已穩定。使用較早版本時,請新增
-Xexplicit-backing-fields編譯器選項,或改用舊的支援欄位模式並配合.asStateFlow()。使用帶有初始設定式的通用
viewModel()函式,將自訂 ViewModel 新增至您的可組合函式:kotlinimport com.example.ui.OrderViewModel @Composable fun CupcakeApp( viewModel: OrderViewModel = viewModel { OrderViewModel() }, ) { // ... }
ViewModel 限定導覽 3 的作用域
在通用程式碼中將 ViewModel 與導覽 3 搭配使用時, 預設情況下 ViewModel 不會自動限定在導覽項目的作用域內。 如果沒有明確限定範圍,每個 ViewModel 都將繫結到 Activity 而非螢幕, 即使在使用者導覽離開後也是如此。
若要按導覽項目限定 ViewModel 作用域並儲存可儲存的 Compose 狀態, 請在定義導覽目標時將導覽 3 項目裝飾器傳遞給 NavDisplay:
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
//...
NavDisplay(
entryDecorators = listOf(
// 儲存每個項目的 Compose 狀態
rememberSaveableStateHolderNavEntryDecorator(),
// 限定每個項目的 ViewModel 作用域
rememberViewModelStoreNavEntryDecorator()
),
backStack = backStack,
entryProvider = entryProvider { }
)ViewModel 與相依注入
相依注入 (DI) 架構允許您根據目前的環境或目標平台,將不同的相依性注入到組件中。 若要管理 ViewModel,您可以使用 Koin、Metro 或任何其他支援 Kotlin Multiplatform 的 DI 架構。
有關相依注入使用的進階範例, 請參閱共用資料存取層教學。
Koin
Koin 是一個執行時 DI 架構,提供 DSL 或註解來配置您的相依性。 若要在 Compose ViewModel 中使用 Koin,請新增 koin-compose-viewmodel 相依性。
接著,您可以使用 koinViewModel() 將 ViewModel 注入可組合函式:
@Composable
fun CupcakeApp(
viewModel: UserViewModel = koinViewModel()
) {
// ...
}詳情請參閱 Koin 關於 ViewModel 支援 以及在 Compose 中注入 ViewModel 的文件。
Metro
Metro 是一個實作為 Kotlin 編譯器外掛程式的編譯期 DI 架構。 若要在 Compose ViewModel 中使用 Metro,請新增 metrox-viewmodel-compose 相依性。
接著,您可以使用 metroViewModel() 將 ViewModel 注入可組合函式:
@Composable
fun CupcakeApp(
viewModel: UserViewModel = metroViewModel()
) {
// ...
}詳情請參閱 MetroX 關於 ViewModel 整合 以及在 Compose 中存取 ViewModel 的文件。
程式碼共用層級
您可以選擇要共用程式碼的哪些部分,以及哪些部分保留為平台特定:
- 若要跨平台共用 UI 和商務邏輯, 請參閱共用邏輯與 UI 教學。
- 若要共用部分程式碼而不共用 UI 實作, 請參閱共用邏輯教學。
以下範例展示了如何在不同程式碼共用層級下使用 ViewModel。 所有範例皆基於上述介紹的 OrderViewModel 類別。
共用 ViewModel 與 UI
在這種方式中,包括 ViewModel 和 UI 在內的所有內容都透過 Compose Multiplatform 共用。 您只需編寫一次應用程式的 UI 程式碼,它即可在所有平台上運作。
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel { OrderViewModel() }
) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
Text("Quantity: ${uiState.quantity}")
Text("Price: ${uiState.price}")
Button(onClick = { viewModel.setQuantity(6) }) {
Text("Set Quantity to '6'")
}
}
}共用 ViewModel 與平台特定 UI
在這種方式中,ViewModel(商務邏輯)是共用的,但平台具有原生 UI 實作。 請在為 Kotlin Multiplatform 設定 ViewModel 中了解更多資訊。
由於在這種情況下 UI 不共用,您可以從 Compose Multiplatform 版本的 ViewModel 程式庫切換到 androidx.lifecycle 程式庫。
在 Gradle 版本目錄中更新相依性:
toml[versions] androidx-viewmodel = "2.10.0" [libraries] androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }在
build.gradle.kts檔案中,將相依性宣告為api,因為它需要匯出到二進位框架:kotlinkotlin { // ... sourceSets { // ... commonMain.dependencies { api(libs.androidx.lifecycle.viewmodel) } // ... } }
Android 實作
在 Android 上,Jetpack Compose 會自動尋找 Activity 提供的 ViewModelStoreOwner 並提供 OrderViewModel。
@Composable
fun AndroidCupcakeApp(
viewModel: OrderViewModel = viewModel { OrderViewModel() }
) {
val uiState by viewModel.uiState.collectAsState()
Column {
Text("Quantity: ${uiState.quantity}")
Text("Price: ${uiState.price}")
Button(onClick = { viewModel.setQuantity(6) }) {
Text("Set Quantity to '6'")
}
}
}iOS 實作
在 iOS 上,沒有內建的 ViewModelStoreOwner,因此必須手動將 ViewModel 的生命週期與 SwiftUI 繫結。 我們建議使用 KMP-ObservableViewModel 程式庫, 它可讓 SwiftUI 直接觀察 Kotlin Multiplatform ViewModel,並處理 iOS 所需的 ViewModel 生命週期/Store Owner 樣板程式碼。
匯出 ViewModel API 以供 Swift 存取:
kotlinlistOf( iosArm64(), iosSimulatorArm64(), ).forEach { it.binaries.framework { export(libs.androidx.lifecycle.viewmodel) baseName = "shared" } }在
commonMain中使用 KMP-ObservableViewModel 的 ViewModel 基底類別和@NativeCoroutinesState註解來定義您的 ViewModel:kotlinimport com.rickclephas.kmp.observableviewmodel.ViewModel import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class OrderViewModel : ViewModel() { private val _uiState = MutableStateFlow(OrderUiState()) @NativeCoroutinesState val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow() fun setQuantity(n: Int) { _uiState.value = _uiState.value.copy(quantity = n) } }在 iOS UI 入口點中使用 ViewModel:
swiftimport SwiftUI import shared import KMPObservableViewModelSwiftUI @main struct iOSCupcakeApp: App { var body: some Scene { WindowGroup { CupcakeView() } } } struct CupcakeView: View { @StateViewModel private var viewModel = OrderViewModel() var body: some View { VStack { Text("Quantity: \(viewModel.uiState.quantity)") Text("Price: \(viewModel.uiState.price)") Button("Set Quantity to '6'") { viewModel.setQuantity(n: 6) } } } }
共用存儲庫/資料層,平台特定 ViewModel 與 UI
另一個選項是僅共用資料與存儲庫層,同時使用平台特定的 ViewModel 實作。 這允許您在每個平台上使用原生模式,例如 Android 的 Hilt 相依注入,或 iOS 搭配 Combine 的 ObservableObject。
建立一個包含資料邏輯的共用存儲庫類別:
kotlinclass OrderRepository { fun calculatePrice(quantity: Int) = "${quantity * 2}.00" }實作平台特定的 ViewModel。
在 Android 上,使用標準 Android ViewModel 並注入存儲庫:
kotlinclass AndroidOrderViewModel( private val repo: OrderRepository ) : ViewModel() { val uiState: StateFlow<OrderUiState> field = MutableStateFlow(OrderUiState()) fun setQuantity(n: Int) { uiState.update { it.copy(quantity = n, price = repo.calculatePrice(n)) } } }在 iOS 上,使用
ObservableObject在 Swift 中原生實作 ViewModel:swiftimport shared class IOSOrderViewModel: ObservableObject { private let repo: OrderRepository @Published var uiState: OrderUiState = OrderUiState() init(repo: OrderRepository) { self.repo = repo } func setQuantity(n: Int32) { uiState = OrderUiState(quantity: n, price: repo.calculatePrice(quantity: n)) } }
實作平台特定的 UI。
在 Android 上:
kotlin@Composable fun AndroidCupcakeApp( viewModel: AndroidOrderViewModel = viewModel { AndroidOrderViewModel(OrderRepository()) } ) { val uiState by viewModel.uiState.collectAsState() Column { Text("Quantity: ${uiState.quantity}") Text("Price: ${uiState.price}") Button(onClick = { viewModel.setQuantity(6) }) { Text("Set Quantity to '6'") } } }在 iOS 上:
swiftstruct IOSCupcakeApp: App { @StateObject var viewModel = IOSOrderViewModel(repo: OrderRepository()) var body: some View { VStack { Text("Quantity: \(viewModel.uiState.quantity)") Text("Price: \(viewModel.uiState.price)") Button("Set Quantity to '6'") { viewModel.setQuantity(n: 6) } } } }
下一步
- 查看完整範例。
- 請參閱為 Kotlin Multiplatform 設定 ViewModel 以獲取更多以 Android 為核心的指引。
- 了解在使用共用 ViewModel 與原生 UI 時,如何將 Compose Multiplatform 與 SwiftUI 整合。
