إعداد ViewModel لـ KMP

يُعدّ 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)
}

تصدير واجهات برمجة تطبيقات ViewModel للوصول إليها من Swift

بشكل تلقائي، لن يتم تصدير أي مكتبة تضيفها إلى قاعدة الرموز البرمجية تلقائيًا إلى إطار العمل الثنائي. إذا لم يتم تصدير واجهات برمجة التطبيقات، ستكون متاحة من إطار العمل الثنائي فقط إذا استخدمتها في الرمز البرمجي المشترك (من مجموعة رموز المصدر iosMain أو commonMain). في هذه الحالة، ستحتوي واجهات برمجة التطبيقات على بادئة الحزمة، على سبيل المثال، ستكون فئة ViewModel متاحة كفئة Lifecycle_viewmodelViewModel. يمكنك الاطّلاع على تصدير التبعيات إلى الملفات الثنائية لمزيد من المعلومات حول تصدير التبعيات.

لتحسين التجربة، يمكنك تصدير تبعية ViewModel إلى إطار العمل الثنائي باستخدام إعداد export في ملف build.gradle.kts حيث تحدّد إطار العمل الثنائي لنظام التشغيل iOS، ما يجعل واجهات برمجة تطبيقات 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. الاعتبار الوحيد هو أنّه لا يمكنك استخدام أي واجهات برمجة تطبيقات خاصة بالنظام الأساسي، وعليك تجريدها. على سبيل المثال، إذا كنت تستخدم Application في Android كمَعلمة دالة إنشاء ViewModel، عليك الانتقال من واجهة برمجة التطبيقات هذه من خلال تجريدها.

يمكنك الاطّلاع على مزيد من المعلومات حول كيفية استخدام الرموز البرمجية الخاصة بالمنصات في الرموز البرمجية الخاصة بالمنصات في 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 على مكافئ مدمج لـ 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 View واستدعاء الدالة 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

بعض واجهات برمجة التطبيقات المتوفّرة على Android غير متاحة في Kotlin Multiplatform.

الدمج مع Hilt

بما أنّ Hilt غير متاح لمشاريع Kotlin Multiplatform، لا يمكنك استخدام ViewModels مباشرةً مع التعليق التوضيحي @HiltViewModel في commonMain sourceSet. في هذه الحالة، عليك استخدام إطار عمل بديل لحقن التبعية، مثل Koin أو kotlin-inject أو Metro أو Kodein. يمكنك العثور على جميع أُطر عمل حقن التبعية التي تعمل مع Kotlin Multiplatform على klibs.io.

مراقبة التدفقات في SwiftUI

لا يمكن تتبُّع تدفقات الكوروتين في SwiftUI مباشرةً. ومع ذلك، يمكنك استخدام مكتبة KMP-NativeCoroutines أو SKIE لإتاحة هذه الميزة.