ה-ViewModel של AndroidX משמש כגשר, ומגדיר חוזה ברור בין הלוגיקה העסקית המשותפת לבין רכיבי ממשק המשתמש. הדפוס הזה עוזר לשמור על עקביות הנתונים בפלטפורמות שונות, ומאפשר להתאים אישית את ממשקי המשתמש כך שיתאימו למראה הייחודי של כל פלטפורמה. אתם יכולים להמשיך לפתח את ממשק המשתמש באמצעות Jetpack Compose ב-Android ו-SwiftUI ב-iOS.
במסמכי התיעוד העיקריים של 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" }
לאחר מכן מוסיפים את הארטיפקט לקובץ build.gradle.kts
של מודול ה-KMP ומצהירים על התלות כ-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)
}
ייצוא ממשקי API של ViewModel לגישה מ-Swift
כברירת מחדל, כל ספרייה שמוסיפים לבסיס הקוד לא מיוצאת אוטומטית למסגרת הבינארית. אם ממשקי ה-API לא מיוצאים, הם זמינים מהמסגרת הבינארית רק אם משתמשים בהם בקוד המשותף (ממערך המקורות iosMain
או commonMain
). במקרה כזה, ממשקי ה-API יכללו את הקידומת של החבילה. לדוגמה, מחלקה ViewModel
תהיה זמינה כמחלקה Lifecycle_viewmodelViewModel
. מידע נוסף על ייצוא תלויות זמין במאמר בנושא ייצוא תלויות לקבצים בינאריים.
כדי לשפר את חוויית השימוש, אפשר לייצא את התלות של ViewModel למסגרת הבינארית באמצעות ההגדרה export
בקובץ build.gradle.kts
שבו מגדירים את המסגרת הבינארית של iOS. כך ממשקי ה-API של ViewModel נגישים ישירות מקוד 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"
}
}
(אופציונלי) שימוש ב-viewModelScope
ב-JVM Desktop
כשמריצים קורוטינות ב-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
מסמכי התיעוד.
שימוש ב-ViewModel מ-commonMain
או מ-androidMain
אין דרישה ספציפית לשימוש במחלקה ViewModel ב-commonMain
או ב-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()
לאחר מכן, בקוד של ממשק המשתמש, אפשר לאחזר את ViewModel כרגיל:
// androidApp/ui/MainScreen.kt @Composable fun MainScreen( viewModel: MainViewModel = viewModel( factory = mainViewModelFactory, ), ) { // observe the viewModel state }
שימוש ב-ViewModel מ-SwiftUI
ב-Android, מחזור החיים של ViewModel מנוהל באופן אוטומטי ומוגדר ל-ComponentActivity
, ל-Fragment
, ל-NavBackStackEntry
(Navigation 2) או ל-rememberViewModelStoreNavEntryDecorator
(Navigation 3). לעומת זאת, ל-SwiftUI ב-iOS אין מקבילה מובנית ל-ViewModel של AndroidX.
כדי לשתף את ViewModel עם אפליקציית SwiftUI, צריך להוסיף קוד הגדרה.
יצירת פונקציה שתעזור עם גנריות
כדי ליצור מופע של ViewModel גנרי, משתמשים בתכונת השתקפות של הפניה למחלקה ב-Android. ב-Objective-C אין תמיכה בכל התכונות של generics ב-Kotlin או ב-Swift, ולכן אי אפשר לאחזר ישירות ViewModel מסוג generic מ-Swift.
כדי לפתור את הבעיה הזו, אפשר ליצור פונקציית עזר שתשתמש ב-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
היא כדי שאפשר יהיה להשתמש במחלקה הזו כ-@StateObject
בקוד 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() } }
הבעלים הזה מאפשר אחזור של כמה סוגים של ViewModel, בדומה ל-Android.
מחזור החיים של אובייקטים מסוג ViewModel מסתיים כשמסיימים את האתחול של המסך שמשתמש ב-IosViewModelStoreOwner
וקוראים ל-deinit
. מידע נוסף על ביטול ההפעלה זמין במסמכי התיעוד הרשמיים.
בשלב הזה, אפשר ליצור מופע של IosViewModelStoreOwner
כ-@StateObject
בתצוגת SwiftUI ולהפעיל את הפונקציה 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
חלק מהממשקי ה-API שזמינים ב-Android לא זמינים ב-Kotlin Multiplatform.
שילוב עם Hilt
Hilt לא זמין לפרויקטים של Kotlin Multiplatform, ולכן אי אפשר להשתמש ישירות ב-ViewModels עם הערה @HiltViewModel
ב-commonMain
sourceSet. במקרה כזה, צריך להשתמש במסגרת DI חלופית, למשל Koin, kotlin-inject, Metro או Kodein. בכתובת klibs.io אפשר למצוא את כל מסגרות ה-DI שפועלות עם Kotlin Multiplatform.
הצגת תהליכים ב-SwiftUI
אין תמיכה ישירה ב-SwiftUI בהצגה של קורוטינות Flows. עם זאת, אפשר להשתמש בספרייה KMP-NativeCoroutines או בספרייה SKIE כדי להפעיל את התכונה הזו.