AndroidX ViewModel đóng vai trò là cầu nối, thiết lập một hợp đồng rõ ràng giữa logic nghiệp vụ dùng chung và các thành phần giao diện người dùng. Mẫu này giúp đảm bảo tính nhất quán của dữ liệu trên các nền tảng, đồng thời cho phép tuỳ chỉnh giao diện người dùng cho từng giao diện riêng biệt của nền tảng. Bạn có thể tiếp tục phát triển giao diện người dùng bằng Jetpack Compose trên Android và SwiftUI trên iOS.
Đọc thêm về lợi ích của việc sử dụng ViewModel và tất cả các tính năng trong tài liệu chính về ViewModel.
Thiết lập phần phụ thuộc
Để thiết lập KMP ViewModel trong dự án, hãy xác định phần phụ thuộc trong tệp libs.versions.toml
:
[versions]
androidx-viewmodel = 2.9.3
[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }
Sau đó, hãy thêm cấu phần phần mềm vào tệp build.gradle.kts
cho mô-đun KMP và khai báo phần phụ thuộc dưới dạng api
, vì phần phụ thuộc này sẽ được xuất sang khung nhị phân:
// 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)
}
Xuất các API ViewModel để truy cập từ Swift
Theo mặc định, mọi thư viện mà bạn thêm vào cơ sở mã sẽ không tự động được xuất sang khung nhị phân. Nếu không được xuất, các API sẽ chỉ có trong khung nhị phân nếu bạn dùng chúng trong mã dùng chung (từ bộ nguồn iosMain
hoặc commonMain
). Trong trường hợp đó, các API sẽ chứa tiền tố gói, ví dụ: lớp ViewModel
sẽ có sẵn dưới dạng lớp Lifecycle_viewmodelViewModel
. Hãy xem phần xuất các phần phụ thuộc sang tệp nhị phân để biết thêm thông tin về cách xuất các phần phụ thuộc.
Để cải thiện trải nghiệm, bạn có thể xuất phần phụ thuộc ViewModel sang khung nhị phân bằng cách sử dụng chế độ thiết lập export
trong tệp build.gradle.kts
. Trong đó, bạn xác định khung nhị phân iOS, giúp các API ViewModel có thể truy cập trực tiếp từ mã Swift giống như từ mã 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"
}
}
(Không bắt buộc) Sử dụng viewModelScope
trên JVM Desktop
Khi chạy các coroutine trong ViewModel, thuộc tính viewModelScope
sẽ được liên kết với Dispatchers.Main.immediate
. Theo mặc định, thuộc tính này có thể không dùng được trên máy tính. Để hoạt động này hoạt động chính xác, hãy thêm phần phụ thuộc kotlinx-coroutines-swing
vào dự án của bạn:
// Optional if you use JVM Desktop
desktopMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:[KotlinX Coroutines version]")
}
Hãy xem tài liệu Dispatchers.Main
để biết thêm thông tin chi tiết.
Sử dụng ViewModel từ commonMain
hoặc androidMain
Không có yêu cầu cụ thể nào về việc sử dụng lớp ViewModel trong commonMain
dùng chung, cũng như từ androidMain
sourceSet. Điều duy nhất cần cân nhắc là bạn không thể sử dụng bất kỳ API dành riêng cho nền tảng nào và bạn cần phải trừu tượng hoá chúng. Ví dụ: nếu đang sử dụng Application
Android làm tham số hàm khởi tạo ViewModel, bạn cần di chuyển khỏi API này bằng cách trừu tượng hoá API.
Bạn có thể xem thêm thông tin về cách sử dụng mã dành riêng cho nền tảng tại mã dành riêng cho nền tảng trong Kotlin Multiplatform.
Ví dụ: trong đoạn mã sau đây là một lớp ViewModel có phương thức khởi tạo, được xác định trong 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()
Sau đó, trong mã giao diện người dùng, bạn có thể truy xuất ViewModel như bình thường:
// androidApp/ui/MainScreen.kt @Composable fun MainScreen( viewModel: MainViewModel = viewModel( factory = mainViewModelFactory, ), ) { // observe the viewModel state }
Sử dụng ViewModel từ SwiftUI
Trên Android, vòng đời ViewModel được tự động xử lý và giới hạn trong ComponentActivity
, Fragment
, NavBackStackEntry
(Navigation 2) hoặc rememberViewModelStoreNavEntryDecorator
(Navigation 3). Tuy nhiên, SwiftUI trên iOS không có thành phần tương đương tích hợp cho AndroidX ViewModel.
Để chia sẻ ViewModel với ứng dụng SwiftUI, bạn cần thêm một số mã thiết lập.
Tạo một hàm để hỗ trợ các thành phần chung
Việc khởi tạo một thực thể ViewModel chung sẽ sử dụng tính năng phản chiếu tham chiếu lớp trên Android. Vì các thành phần chung Objective-C không hỗ trợ tất cả các tính năng của Kotlin hoặc Swift, nên bạn không thể trực tiếp truy xuất ViewModel thuộc loại chung từ Swift.
Để giải quyết vấn đề này, bạn có thể tạo một hàm trợ giúp sẽ dùng ObjCClass
thay vì kiểu chung, sau đó dùng getOriginalKotlinClass
để truy xuất lớp ViewModel cần khởi tạo:
// 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] }
Sau đó, khi muốn gọi hàm từ Swift, bạn có thể viết một hàm chung thuộc loại T : ViewModel
và sử dụng T.self
. Hàm này có thể truyền ObjCClass
vào hàm resolveViewModel
.
Kết nối phạm vi ViewModel với Vòng đời SwiftUI
Bước tiếp theo là tạo một IosViewModelStoreOwner
triển khai các giao diện (giao thức) ObservableObject
và ViewModelStoreOwner
. Lý do cho ObservableObject
là để có thể sử dụng lớp này làm @StateObject
trong mã SwiftUI:
// 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() } }
Đối tượng này cho phép truy xuất nhiều loại ViewModel, tương tự như trên Android.
Vòng đời của những ViewModel đó sẽ bị xoá khi màn hình sử dụng IosViewModelStoreOwner
được huỷ khởi tạo và gọi deinit
. Bạn có thể tìm hiểu thêm về quá trình huỷ khởi tạo trong tài liệu chính thức.
Tại thời điểm này, bạn chỉ cần tạo thực thể IosViewModelStoreOwner
dưới dạng @StateObject
trong một SwiftUI View và gọi hàm viewModel
để truy xuất một 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 } }
Không có trong Kotlin Multiplatform
Một số API có trên Android không có trong Kotlin Multiplatform.
Tích hợp với Hilt
Vì Hilt không dùng được cho các dự án Kotlin Multiplatform, nên bạn không thể trực tiếp dùng ViewModel có chú thích @HiltViewModel
trong commonMain
sourceSet. Trong trường hợp đó, bạn cần sử dụng một số khung DI thay thế, chẳng hạn như Koin, kotlin-inject, Metro hoặc Kodein. Bạn có thể tìm thấy tất cả các khung DI hoạt động với Kotlin Multiplatform tại klibs.io.
Quan sát các Flow trong SwiftUI
SwiftUI không hỗ trợ trực tiếp việc quan sát các luồng coroutine. Tuy nhiên, bạn có thể sử dụng thư viện KMP-NativeCoroutines hoặc SKIE để cho phép tính năng này.