AndroidX ViewModel משמש כגשר, ויוצר חוזה ברור בין הלוגיקה העסקית המשותפת לבין רכיבי ממשק המשתמש. התבנית הזו עוזרת להבטיח עקביות בנתונים בין הפלטפורמות, ומאפשרת להתאים אישית את ממשקי המשתמש למראה הייחודי של כל פלטפורמה. אתם יכולים להמשיך לפתח את ממשק המשתמש באמצעות Jetpack Compose ב-Android ו-SwiftUI ב-iOS.
במסמכי התיעוד הראשיים של ViewModel אפשר לקרוא מידע נוסף על היתרונות של השימוש ב-ViewModel ועל כל התכונות.
הגדרת יחסי תלות
כדי להגדיר את KMP ViewModel בפרויקט, מגדירים את התלות בקובץ libs.versions.toml:
[versions]
androidx-viewmodel = 2.11.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
כברירת מחדל, כל ספרייה שתוסיפו לבסיס הקוד לא תיוצא באופן אוטומטי ל-framework הבינארי. אם ממשקי ה-API לא מיוצאים, הם זמינים מה-framework הבינארי רק אם משתמשים בהם בקוד המשותף (מתוך קבוצת המקורות iosMain או commonMain). במקרה כזה, ממשקי ה-API יכילו את הקידומת של החבילה. לדוגמה, מחלקה מסוג ViewModel תהיה זמינה כמחלקה מסוג Lifecycle_viewmodelViewModel. למידע נוסף על ייצוא יחסי תלות, קראו את המאמר ייצוא יחסי תלות לקבצים בינאריים.
כדי לשפר את החוויה, אתם יכולים לייצא את התלות של ViewModel ל-framework הבינארי באמצעות ההגדרה export בקובץ build.gradle.kts שבו אתם מגדירים את ה-framework הבינארי של 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
כשמריצים פונקציות coroutine ב-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 המשותף, וגם לא מ-sourceSet של androidMain. הדבר היחיד שצריך להביא בחשבון הוא שאי אפשר להשתמש בממשקי API ספציפיים לפלטפורמה, וצריך להפשיט אותם. לדוגמה, אם אתם משתמשים ב-Application של Android כפרמטר של בנאי ViewModel, אתם צריכים להפסיק להשתמש ב-API הזה על ידי הפשטה שלו.
מידע נוסף על השימוש בקוד ספציפי לפלטפורמה זמין במאמר קוד ספציפי לפלטפורמה ב-Kotlin Multiplatform.
לדוגמה, בקטע הקוד הבא יש מחלקה של ViewModel עם ה-factory שלה, שמוגדרת ב-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 (ניווט 2) או ל-rememberViewModelStoreNavEntryDecorator (ניווט 3). אבל ב-SwiftUI ב-iOS אין מקבילה מובנית ל-AndroidX ViewModel.
כדי לשתף את 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.
מחזור החיים של ה-ViewModels האלה נמחק כשהמסך שמשתמש ב-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 ב-sourceSet של commonMain. במקרה כזה, צריך להשתמש במסגרת חלופית של הזרקת תלות, למשל Koin, kotlin-inject, Metro או Kodein. אתם יכולים למצוא את כל מסגרות ה-DI שפועלות עם Kotlin Multiplatform בכתובת klibs.io.
צפייה בתהליכים ב-SwiftUI
אין תמיכה ישירה בתצפית על תהליכי Coroutine ב-SwiftUI. עם זאת, אתם יכולים להשתמש בספרייה KMP-NativeCoroutines או SKIE כדי לאפשר את התכונה הזו.