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

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

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

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

مزايا ViewModel

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

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

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

الاستمرارية

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

النطاق

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

تكون مجموعة من الفئات إما فئات فرعية مباشرة أو غير مباشرة من واجهة ViewModelStoreOwner. الفئات الفرعية المباشرة هي ComponentActivity وFragment وNavBackStackEntry. للاطّلاع على قائمة كاملة بالفئات الفرعية غير المباشرة، راجِع مرجع ViewModelStoreOwner.

عند إيقاف الجزء أو النشاط الذي تم تحديد نطاق ViewModel له، يستمر العمل غير المتزامن في ViewModel الذي تم تحديد نطاقه له. هذا هو مفتاح الاستمرار.

لمزيد من المعلومات، يُرجى الاطّلاع على القسم أدناه حول دورة حياة ViewModel.

SavedStateHandle

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

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

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

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

Jetpack Compose

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

أهم شيء يجب تذكّره عند استخدام ViewModel مع Compose هو أنّه لا يمكنك تحديد نطاق ViewModel إلى عنصر قابل للإنشاء. ويرجع ذلك إلى أنّ العنصر القابل للإنشاء ليس ViewModelStoreOwner. سيؤدي استخدام مثيلَين من العنصر القابل للإنشاء نفسه في Composition، أو عنصرَين مختلفَين قابلَين للإنشاء يصلان إلى نوع ViewModel نفسه ضمن ViewModelStoreOwner نفسه، إلى تلقّي المثيل نفسه من ViewModel، وهو ما لا يُتوقّع حدوثه في كثير من الأحيان.

للحصول على مزايا ViewModel في Compose، استضِف كل شاشة في Fragment أو Activity، أو استخدِم Compose Navigation واستخدِم ViewModels في الدوال البرمجية القابلة للإنشاء بالقرب من وجهة التنقّل قدر الإمكان. ويرجع ذلك إلى أنّه يمكنك تحديد نطاق ViewModel ليشمل وجهات التنقّل ورسوم التنقّل البيانية والأنشطة واللقطات.

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

تنفيذ ViewModel

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

Kotlin

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,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

يمكنك بعد ذلك الوصول إلى ViewModel من نشاط على النحو التالي:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

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، عند إزالته من سجلّ الرجوع.

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

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

يوضّح دورة حياة 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 إلى فئات أو دوال أو مكونات أخرى في واجهة المستخدم. وبما أنّ المنصة تديرها، عليك إبقاؤها قريبة قدر الإمكان. بالقرب من الدالة المركّبة على مستوى النشاط أو الجزء أو الشاشة يمنع ذلك المكوّنات ذات المستوى الأدنى من الوصول إلى بيانات ومنطق أكثر مما تحتاج إليه.

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

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

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

مراجع إضافية

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

المستندات

نماذج