نظرة عامة على ViewModel   جزء من Android Jetpack

تجربة Kotlin Multiplatform
تتيح Kotlin Multiplatform مشاركة منطق النشاط التجاري مع منصات أخرى. تعرَّف على كيفية إعداد ViewModel واستخدامه في KMP

فئة ViewModel هي عنصر الاحتفاظ بالحالة على مستوى منطق النشاط التجاري أو الشاشة. تعرض هذه الطبقة الحالة لواجهة المستخدم وتغلف منطق النشاط التجاري ذي الصلة. وتتمثّل ميزته الأساسية في أنّه يخزّن الحالة مؤقتًا ويحتفظ بها عند إجراء تغييرات في الإعدادات. وهذا يعني أنّه ليس على واجهة المستخدم جلب البيانات مرة أخرى عند التنقّل بين الأنشطة أو عند إجراء تغييرات في الإعدادات، مثل تدوير الشاشة.

لمزيد من المعلومات حول الجهات الحكومية، يُرجى الاطّلاع على إرشادات الجهات الحكومية. وبالمثل، للحصول على مزيد من المعلومات حول طبقة واجهة المستخدم بشكل عام، يمكنك الاطّلاع على إرشادات طبقة واجهة المستخدم.

مزايا ViewModel

البديل عن ViewModel هو فئة عادية تحتوي على البيانات التي تعرضها في واجهة المستخدم. ويمكن أن يصبح ذلك مشكلة عند التنقّل بين الأنشطة أو وجهات التنقّل. سيؤدي ذلك إلى إتلاف البيانات إذا لم يتم تخزينها باستخدام آلية حفظ حالة المثيل. توفّر ViewModel واجهة برمجة تطبيقات ملائمة لاستمرار البيانات، ما يحلّ هذه المشكلة.

بدلاً من ذلك، توفّر Compose لحاملي الحالة البحتة إمكانات retain التي تتيح لفئات عادية البقاء على قيد التشغيل عند حدوث تغييرات في الإعدادات بدون البنية الأساسية الكاملة لـ ViewModel. على الرغم من أنّ كلا الآليتَين تساعدان في الاحتفاظ بالحالة، من الأفضل عمومًا توفير ViewModel لنسخة محتفظ بها بدلاً من العكس، لأنّ دورات حياتهما وسلوكيات التنظيف تختلف.

تتمثّل الميزتان الرئيسيتان لفئة ViewModel في ما يلي:

  • يتيح لك الاحتفاظ بحالة واجهة المستخدم.
  • ويوفّر إمكانية الوصول إلى منطق النشاط التجاري.

الاستمرارية

يتيح ViewModel إمكانية استمرار البيانات من خلال كل من الحالة التي يحتفظ بها ViewModel والعمليات التي يبدأها ViewModel. ويعني التخزين المؤقت أنّه لن تحتاج إلى جلب البيانات مرة أخرى عند إجراء تغييرات شائعة في الإعدادات، مثل تدوير الشاشة.

النطاق

عند إنشاء مثيل من ViewModel، عليك تمرير عنصر ينفّذ واجهة ViewModelStoreOwner. قد يكون هذا النوع وجهة تنقّل أو رسمًا بيانيًا للتنقّل أو نشاطًا أو أي نوع آخر ينفّذ الواجهة. يمكنك أيضًا تحديد نطاق ViewModel مباشرةً إلى عنصر قابل للإنشاء باستخدام واجهة برمجة التطبيقات rememberViewModelStoreOwner. يتم بعد ذلك تحديد نطاق ViewModel ضمن Lifecycle الخاص بـ ViewModelStoreOwner. ويظل في الذاكرة إلى أن يختفي ViewModelStoreOwner نهائيًا (مثلما يحدث عند خروج مالك العنصر القابل للإنشاء من Composition).

تكون مجموعة من الفئات إما فئات فرعية مباشرة أو غير مباشرة من واجهة ViewModelStoreOwner. الفئتان الفرعيتان المباشرتان هما ComponentActivity وNavBackStackEntry. للاطّلاع على قائمة كاملة بالفئات الفرعية غير المباشرة، راجِع مرجع ViewModelStoreOwner. لتحديد نطاق ViewModels لعناصر فردية في LazyList أو Pager، استخدِم rememberViewModelStoreProvider() لرفع إدارة المالك إلى العنصر الرئيسي.

عندما يخضع نشاط المضيف لتغيير في الإعدادات، يستمر العمل غير المتزامن في ViewModel، سواء كان نطاقه النشاط أو عنصرًا قابلاً للإنشاء محدّدًا. هذا هو مفتاح الاستمرار.

لمزيد من المعلومات، راجِع قسم مراحل نشاط ViewModel أدناه، وواجهات برمجة التطبيقات لتحديد نطاق ViewModel، ودليل نقل القيمة في Jetpack Compose.

SavedStateHandle

تتيح لك السمة SavedStateHandle إمكانية الاحتفاظ بالبيانات ليس فقط عند حدوث تغييرات في الإعدادات، بل أيضًا عند إيقاف العملية نهائيًا. أي أنّه يتيح لك الحفاظ على حالة واجهة المستخدم كما هي حتى عندما يغلق المستخدم التطبيق ويفتحه في وقت لاحق.

لمزيد من المعلومات حول حفظ حالة واجهة المستخدم، راجِع مقالة حفظ حالة واجهة المستخدم في Compose.

الوصول إلى منطق النشاط التجاري

على الرغم من أنّ الغالبية العظمى من منطق النشاط التجاري مضمّنة في طبقة البيانات، يمكن أن تحتوي طبقة واجهة المستخدم أيضًا على منطق النشاط التجاري. قد يحدث ذلك عند دمج بيانات من مستودعات متعددة لإنشاء حالة واجهة المستخدم على الشاشة، أو عندما لا يتطلّب نوع معيّن من البيانات طبقة بيانات.

‫ViewModel هو المكان المناسب للتعامل مع منطق النشاط التجاري في طبقة واجهة المستخدم. تتولّى ViewModel أيضًا مهمة معالجة الأحداث وتفويضها إلى طبقات أخرى من التسلسل الهرمي عند الحاجة إلى تطبيق منطق النشاط التجاري لتعديل بيانات التطبيق.

تنفيذ ViewModel

في ما يلي مثال على تنفيذ ViewModel لشاشة تتيح للمستخدم رمي النرد.

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

يمكنك بعد ذلك الوصول إلى ViewModel من عنصر قابل للإنشاء على مستوى الشاشة على النحو التالي:

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

استخدام الروتينات المشتركة مع ViewModel

يتضمّن ViewModel إمكانية استخدام إجراءات Kotlin الفرعية. ويمكنه الحفاظ على العمليات غير المتزامنة بالطريقة نفسها التي يحافظ بها على حالة واجهة المستخدم.

لمزيد من المعلومات، يُرجى الاطّلاع على مقالة استخدام الكوروتينات في Kotlin مع &quot;مكوّنات بنية Android&quot;.

مراحل نشاط ViewModel

ترتبط مراحل نشاط ViewModel ارتباطًا مباشرًا بنطاقها. يبقى ViewModel في الذاكرة إلى أن يختفي ViewModelStoreOwner الذي يندرج ضمن نطاقه. قد يحدث ذلك في السياقات التالية:

  • في حالة النشاط، يكون ذلك عند الانتهاء منه.
  • في حالة إدخال Navigation، عند إزالته من الأنشطة السابقة.
  • في حالة عنصر قابل للإنشاء، عند خروجه من Composition يمكنك استخدام rememberViewModelStoreOwner لتحديد نطاق ViewModel مباشرةً إلى جزء عشوائي من واجهة المستخدم (مثل Pager أو LazyList).

وهذا يجعل ViewModels حلاً رائعًا لتخزين البيانات التي لا تتأثر بالتغييرات في الإعدادات.

يوضِّح الشكل 1 حالات دورة الحياة المختلفة لنشاط أثناء تدويره ثم إنهاءه. يوضّح الرسم التوضيحي أيضًا مدة صلاحية ViewModel بجانب مراحل النشاط المرتبطة بها. يوضّح هذا المخطّط البياني على وجه التحديد حالات النشاط.

توضّح هذه الصورة دورة حياة ViewModel عند تغيير حالة النشاط.
الشكل 1. حالات مراحل النشاط في النشاط وViewModel

عادةً ما تطلب ViewModel في المرة الأولى التي يستدعي فيها النظام طريقة onCreate() لأحد عناصر الأنشطة. قد يستدعي النظام الدالة onCreate() عدة مرات طوال مدة نشاط التطبيق، مثلما يحدث عند تدوير شاشة الجهاز. يبقى ViewModel متاحًا منذ أن تطلب ViewModel للمرة الأولى إلى أن ينتهي النشاط ويتم إيقافه.

محو الاعتماديات في ViewModel

يستدعي ViewModel الطريقة onCleared عندما يمحوها ViewModelStoreOwner أثناء مراحل نشاطها. يتيح لك ذلك إزالة أي مهام أو تبعيات تتبع دورة حياة ViewModel.

يعرض المثال التالي بديلاً عن viewModelScope. ‫viewModelScope هي CoroutineScope مضمّنة تتّبع تلقائيًا مراحل نشاط ViewModel. وتستخدمها ViewModel لتفعيل العمليات المرتبطة بالنشاط التجاري. إذا أردت استخدام نطاق مخصّص بدلاً من viewModelScope لإجراء اختبار أسهل، يمكن أن تتلقّى ViewModel CoroutineScope كعنصر تابع في الدالة الإنشائية. عندما تزيل ViewModelStoreOwner ViewModel في نهاية مراحل نشاطها، تلغي ViewModel أيضًا CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

بدءًا من الإصدار 2.5 من Lifecycle والإصدارات الأحدث، يمكنك تمرير عنصر واحد أو أكثر من عناصر Closeable إلى الدالة الإنشائية الخاصة بـ ViewModel التي يتم إغلاقها تلقائيًا عند محو مثيل ViewModel.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

أفضل الممارسات

في ما يلي بعض أفضل الممارسات الأساسية التي يجب اتّباعها عند تنفيذ ViewModel:

  • بسبب نطاقها، استخدِم ViewModels كتفاصيل تنفيذ لـ عنصر الاحتفاظ بالحالة على مستوى الشاشة. لا تستخدِمها كحاويات للحالة لمكوّنات واجهة المستخدم القابلة لإعادة الاستخدام، مثل مجموعات الشرائح أو النماذج. بخلاف ذلك، ستحصل على مثيل ViewModel نفسه في الاستخدامات المختلفة لمكوّن واجهة المستخدم نفسه ضمن ViewModelStoreOwner نفسه ما لم تستخدم مفتاح نموذج عرض صريحًا لكل شريحة.
  • يجب ألا تعرف ViewModels تفاصيل تنفيذ واجهة المستخدم. حاوِل أن تكون أسماء الطرق التي تعرضها واجهة برمجة التطبيقات الخاصة بـ ViewModel وأسماء حقول حالة واجهة المستخدم عامة قدر الإمكان. بهذه الطريقة، يمكن أن تستوعب ViewModel أي نوع من واجهات المستخدم، سواء كان هاتفًا جوّالاً أو جهازًا قابلاً للطي أو جهازًا لوحيًا أو حتى جهاز Chromebook.
  • بما أنّها قد تبقى نشطة لفترة أطول من ViewModelStoreOwner، يجب ألا تحتوي ViewModels على أي مراجع لواجهات برمجة التطبيقات ذات الصلة بدورة الحياة، مثل Context أو Resources، وذلك لمنع تسرُّب الذاكرة.
  • لا تمرِّر ViewModels إلى فئات أو دوال أو مكونات أخرى في واجهة المستخدم. بما أنّ النظام الأساسي يديرها، عليك إبقاؤها قريبة قدر الإمكان منه، أي بالقرب من النشاط أو الدالة المركّبة على مستوى الشاشة أو وجهة Navigation. يمنع ذلك المكوّنات ذات المستوى الأدنى من الوصول إلى بيانات ومنطق أكثر مما تحتاج إليه.

معلومات إضافية

مع ازدياد تعقيد بياناتك، قد تختار إنشاء فئة منفصلة فقط لتحميل البيانات. الغرض من ViewModel هو تغليف البيانات لوحدة التحكّم في واجهة المستخدم للسماح للبيانات بالبقاء بعد إجراء تغييرات في الإعداد. للحصول على معلومات حول كيفية تحميل البيانات والاحتفاظ بها وإدارتها عند إجراء تغييرات في الإعدادات، يمكنك الاطّلاع على حالات واجهة المستخدم المحفوظة.

يقترح دليل تصميم تطبيقات Android إنشاء فئة مستودع للتعامل مع هذه الوظائف.

مراجع إضافية

لمزيد من المعلومات حول الفئة ViewModel، يُرجى الاطّلاع على المراجع التالية.

الوثائق

مشاهدة المحتوى

نماذج