ה-ViewModel של AndroidX משמש כגשר, ומגדיר חוזה ברור בין הלוגיקה העסקית המשותפת לבין רכיבי ממשק המשתמש. הדפוס הזה עוזר לשמור על עקביות הנתונים בפלטפורמות שונות, ומאפשר להתאים אישית את ממשקי המשתמש כך שיתאימו למראה הייחודי של כל פלטפורמה. אתם יכולים להמשיך לפתח את ממשק המשתמש באמצעות Jetpack Compose ב-Android ו-SwiftUI ב-iOS.
במסמכי התיעוד העיקריים של ViewModel אפשר לקרוא מידע נוסף על היתרונות של השימוש ב-ViewModel ועל כל התכונות שלו.
הגדרת יחסי תלות
כדי להגדיר את KMP ViewModel בפרויקט, מגדירים את התלות בקובץ libs.versions.toml:
[versions]
androidx-viewmodel = 2.10.0
[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 ספציפיים לפלטפורמה, וצריך להשתמש בממשקי 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 אין תמיכה בכל התכונות של גנריקות ב-Kotlin או ב-Swift, ולכן אי אפשר לאחזר ישירות ViewModel מסוג גנרי מ-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 כדי להפעיל את התכונה הזו.