إعداد ViewModel لـ KMP

تعمل فئة 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)
}

تصدير واجهات برمجة تطبيقات 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 sourceSet. الاعتبار الوحيد هو أنّه لا يمكنك استخدام أي واجهات برمجة تطبيقات خاصة بمنصّة معيّنة، وعليك تجريدها. على سبيل المثال، إذا كنت تستخدم 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 واستدعاء الدالة 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. يمكنك العثور على جميع أُطر عمل DI التي تتوافق مع Kotlin Multiplatform على klibs.io.

مراقبة مسارات العمل في SwiftUI

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