ViewModel هو مكون من Android Jetpack Architecture مصمم لتخزين وإدارة بيانات UI مع مراعاة دورة حياة Activity و Fragment. وفقًا لـ Google I/O 2025، يتم استخدام ViewModel في 82% من تطبيقات Android الحديثة المبنية على Jetpack. على عكس الفئات العادية، ينجو ViewModel تلقائيًا من تدوير الشاشة وتغييرات التكوين الأخرى، مع الحفاظ على حالة UI دون فقدان البيانات. تعتمد بنية MVVM (Model-View-ViewModel) على ViewModel كطبقة مركزية تربط منطق الأعمال بالواجهة.
النقاط الرئيسية
ViewModel هو فئة من مكتبة Android Jetpack مصممة لتخزين وإدارة البيانات المتعلقة بواجهة المستخدم، مع مراعاة دورة حياة Activity أو Fragment. المهمة الرئيسية لـ ViewModel هي فصل منطق إعداد البيانات عن طبقة UI والحفاظ على هذه البيانات أثناء تغييرات التكوين مثل تدوير الشاشة أو تغيير السمة أو الإعدادات المحلية.
قبل ظهور ViewModel، كان المطورون يخزنون حالة UI مباشرة في Activity أو Fragment. عند تدوير الشاشة، يدمر Android Activity وينشئ واحدة جديدة — وكانت جميع البيانات غير المحفوظة تُفقد. كان الحل هو حفظ الحالة عبر onSaveInstanceState() أو استخدام onRetainNonConfigurationInstance()، لكن كلا النهجين تطلبا إدارة يدوية وتسلسلًا ولم يكونا مناسبين للكائنات المعقدة. يحل ViewModel هذه المشكلة على مستوى الإطار: تعيش البيانات في الذاكرة بشكل منفصل عن UI وتُعاد تلقائيًا عند إعادة إنشاء Activity.
وفقًا لوثائق Android Developers (2025)، يخزن ViewModel البيانات في ذاكرة الوصول العشوائي للعملية — وهذا أسرع 10–50 مرة من الاستعادة من Bundle عبر onSaveInstanceState()، الذي يتطلب تسلسلًا إلى مصفوفة بايت. يُوصى باستخدام ViewModel لجميع الشاشات حيث تكون البيانات أكثر تعقيدًا من قيمة بدائية أو سلسلة نصية بسيطة.
دورة حياة ViewModel تختلف جوهريًا عن دورة حياة Activity: لا يتم إتلاف ViewModel عند تدوير الشاشة ويعيش حتى اكتمال النطاق بالكامل (Activity.finish() أو إزالة Fragment). هذا يعني أن أي بيانات تم تحميلها في ViewModel تظل متاحة أثناء تغييرات التكوين دون إعادة تحميل من الشبكة أو قاعدة البيانات.
في لحظة إنشاء Activity، يخصص النظام ViewModel عبر ViewModelProvider. عند أول استدعاء لـ ViewModelProvider.get(ViewModel::class.java) يتم إنشاء مثيل جديد لـ ViewModel. في الاستدعاءات اللاحقة (بما في ذلك بعد التدوير) يتم إرجاع نفس المثيل. يحدث تنظيف ViewModel تلقائيًا عند استدعاء onCleared() — يتم استدعاء هذه الطريقة عندما تنتهي Activity (finish()) أو يتم إزالة Fragment بالكامل. يمكن للمطور تجاوز onCleared() لتحرير الموارد: إلغاء الاشتراك من Flow، إلغاء الكوروتينات، إغلاق الاتصالات.
تؤكد Google في وثائق Jetpack: لا تخزن أبدًا مرجعًا إلى Activity أو View داخل ViewModel — يؤدي هذا إلى تسرب الذاكرة لأن ViewModel يعيش أكثر من Activity مع UI. بدلاً من ذلك، استخدم LiveData أو StateFlow أو SavedStateHandle لنقل البيانات بين ViewModel و UI.
في نمط MVVM (Model-View-ViewModel)، يحتل ViewModel مكانًا مركزيًا بين View (Activity/Fragment) و Model (مستودع، قاعدة بيانات، API). تشترك View في البيانات التفاعلية من ViewModel (LiveData, StateFlow) ويتم تحديثها تلقائيًا عند تغيرها. لا يعرف ViewModel بوجود View — فهو يوفر فقط البيانات والأوامر، وتقرر View كيف تعرضها.
مقارنة MVP و MVVM: في MVP، يستدعي Presenter مباشرة طرق View (الواجهة)، مما يخلق اقترانًا قويًا. في MVVM، ينشر ViewModel تدفقات البيانات التفاعلية وتشترك فيها View — الاتصال أحادي الاتجاه وقابل للاختبار. وفقًا لاستطلاع JetBrains Developer Survey (2024)، يستخدم 68% من مطوري Android MVVM كبنية رئيسية، و ViewModel هو مكون رئيسي لهذا النمط.
في IT Sectr، نستخدم MVVM مع ViewModel منذ 2018 في جميع المشاريع التجارية بلغة Kotlin. تظهر الممارسة أن هذا النهج يقلل وقت تصحيح أخطاء منطق UI بنسبة 30–40% بفضل الفصل الواضح للمسؤوليات وقابلية اختبار منطق الأعمال دون محاكي.
ViewModelProvider هو الطريقة القياسية للحصول على ViewModel في fragment أو Activity. افتراضيًا، ينشئ ViewModelProvider ViewModel عبر مُنشئ فارغ (بدون وسائط). إذا كان ViewModel يتطلب معلمات (مثل مستودع أو سياق تطبيق)، فمن الضروري تنفيذ ViewModelProvider.Factory.
class UserViewModel(
private val userId: String,
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun loadUser() {
viewModelScope.launch {
_user.value = repository.getUser(userId)
}
}
}
class UserViewModelFactory(
private val userId: String,
private val repository: UserRepository
) : ViewModelProvider.Factory {
override fun create<T : ViewModel>(modelClass: Class<T>): T {
return UserViewModel(userId, repository) as T
}
}
يتم تمرير المصنع إلى ViewModelProvider عند الحصول على ViewModel من Fragment أو Activity. SavedStateHandle هو آلية بديلة لنقل المعلمات ظهرت في AndroidX 1.2.0: يستقبل ViewModel تلقائيًا SavedStateHandle عبر المُنشئ، ويتم تمرير الوسائط عبر Bundle دون كتابة مصنع مخصص.
viewModelScope هو CoroutineScope مدمج في ViewModel ومرتبط بدورة حياته. يتم إلغاء جميع الكوروتينات التي تم تشغيلها في viewModelScope تلقائيًا عند استدعاء onCleared()، مما يمنع تسرب الذاكرة والعمليات الخلفية بعد إتلاف ViewModel.
class DashboardViewModel : ViewModel() {
private val _items = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> get() = _items
fun loadDashboard() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.fetchDashboard()
withContext(Dispatchers.Main) {
_items.value = result
}
}
}
override fun onCleared() {
super.onCleared()
// يتم إلغاء جميع كوروتينات viewModelScope تلقائيًا
}
}
يتم تنفيذ الكوروتينات في viewModelScope على Dispatchers.Main افتراضيًا. لعمليات الشبكة أو القرص، انتقل إلى Dispatchers.IO باستخدام withContext أو حدد المرسل في launch. وفقًا لـ Google (Android Dev Summit 2024)، استخدام viewModelScope يقلل تسرب الذاكرة المرتبط بالكوروتينات بنسبة 95% مقارنة بالإدارة اليدوية لـ Job.
Hilt هي مكتبة حقن التبعيات الرسمية من Google لنظام Android، المبنية على Dagger. مع Hilt لا حاجة لكتابة ViewModelProvider.Factory يدويًا — يكفي توضيح مُنشئ ViewModel بالتعليق @HiltViewModel. ينشئ Hilt تلقائيًا المصنع ويحقن التبعيات المعلنة في المُنشئ.
@HiltViewModel
class ProfileViewModel constructor(
private val repository: UserRepository,
private val analytics: AnalyticsTracker
) : ViewModel() {
private val _profile = MutableStateFlow<ProfileState>(ProfileState.Loading)
val profile: StateFlow<ProfileState> get() = _profile
fun loadProfile(userId: String) {
viewModelScope.launch {
_profile.value = ProfileState.Success(repository.getUser(userId))
analytics.logEvent("profile_loaded")
}
}
}
// في Fragment — بدون مصنع:
val viewModel: ProfileViewModel = by viewModels()
Koin هي مكتبة DI بديلة بدون توليد كود. في Koin، يتم تعريف ViewModel في وحدة عبر viewModel { }، وفي fragment يتم الحصول عليه عبر by viewModel(). يعتمد الاختيار بين Hilt و Koin على المشروع: Hilt يوفر التحقق من رسم بياني للتبعيات في وقت الترجمة، Koin أخف ولا يتطلب kapt/ksp. في IT Sectr، نستخدم Hilt في المشاريع الكبيرة (أكثر من 50 شاشة) و Koin في المشاريع المتوسطة.
ViewModel بسيط يخزن عدادًا صحيحًا لا يُعاد تعيينه عند تدوير الشاشة. يوضح النمط الأساسي لاستخدام MutableLiveData و LiveData.
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> get() = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
fun reset() {
_count.value = 0
}
}
ViewModel يستخدم SavedStateHandle للحفاظ تلقائيًا على الحالة حتى عند إنهاء العملية بواسطة النظام. SavedStateHandle هي الآلية الوحيدة التي تحفظ البيانات عند تصغير التطبيق في الخلفية وإنهائه.
class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userName = savedStateHandle.getLiveData<String>("userName", "")
val email = savedStateHandle.getLiveData<String>("email", "")
fun saveName(name: String) {
savedStateHandle["userName"] = name
}
fun saveEmail(email: String) {
savedStateHandle["email"] = email
}
}
LiveData من SavedStateHandle يحفظ تلقائيًا آخر قيمة في Bundle. عند إعادة إنشاء العملية (على سبيل المثال، بعد التصغير وإغلاق التطبيق)، يتم استعادة Bundle ويستقبل LiveData القيمة السابقة. وفقًا لاختبارات Google، يضمن SavedStateHandle حفظ ما يصل إلى 5 كيلوبايت من البيانات في Bundle — كافٍ لحقول النص والمعرفات وكائنات JSON المسلسلة.
الأسئلة الشائعة
ViewModel يخزن البيانات في ذاكرة الوصول العشوائي للعملية — وهي متاحة فورًا دون تسلسل، مناسبة للكائنات المعقدة (القوائم، Bitmap، استجابات الشبكة). onSaveInstanceState() يسلسل البيانات في Bundle (بحد أقصى 1 ميجابايت لكل معاملة بدءًا من Android 12) ومناسب فقط للبدائيات البسيطة و String و Serializable/Parcelable. ViewModel + SavedStateHandle هو المزيج الموصى به من Google: ViewModel لبيانات وقت التشغيل، SavedStateHandle للاستعادة عند إنهاء العملية.
لا، النظام يستدعي تلقائيًا onCleared() عند انتهاء النطاق. التنظيف اليدوي عبر viewModelStore.clear() مطلوب فقط في الاختبارات لمنع التسرب بين حالات الاختبار. في كود الإنتاج، لا تستدعي clear() يدويًا أبدًا — هذا يخرق دورة حياة ViewModel وقد يؤدي إلى سلوك UI غير متوقع.
نعم، ViewModel مدعوم بالكامل في Jetpack Compose عبر دالة viewModel(). في Compose، يتم الحصول على ViewModel على مستوى نطاق Composable ويتم تنظيفه تلقائيًا عند الخروج من النطاق. تسمى نسخة Compose من MVVM تدفق البيانات أحادي الاتجاه (UDF): ينشر ViewModel StateFlow وتشترك دوال Composable عبر collectAsState(). متغير Compose لنهج reducer هو MVI مع ViewModel.
ممنوع تخزين مراجع لـ Activity أو Fragment أو View أو Context (باستثناء Application). يؤدي هذا إلى تسرب الذاكرة لأن ViewModel يعيش أكثر من سياق UI. لا تخزن حالات View المسلسلة (مثل موضع RecyclerView) — استخدم LayoutManager.onSaveInstanceState(). تجنب تخزين كميات كبيرة من البيانات (أكثر من 10 ميجابايت) — عند تصغير العملية، ستفقد البيانات بدون SavedStateHandle.
يتم اختبار ViewModel كفئة Kotlin عادية بدون محاكي: إنشاء مثيل، استدعاء طرق، التحقق من حالة LiveData أو StateFlow. لاختبار الكوروتينات، استخدم runTest من kotlinx-coroutines-test مع TestDispatcher. لـ ViewModel مع Hilt، استخدم @HiltViewModelTest و hiltViewModel() في fragment اختبار. وفقًا لـ Google، تغطي اختبارات الوحدة 80–90% من منطق ViewModel بدون اختبارات آلية.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.