הגדרת ViewModel ל-KMP

ה-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 כדי להפעיל את התכונה הזו.