MVVM (Model-View-ViewModel) هو نمط معماري يستبدل فيه ViewModel الـ Presenter ويستخدم آليات تفاعلية للتواصل مع View: ObservableObject في SwiftUI، LiveData/StateFlow في Android. لا يحتوي ViewModel على مرجع إلى View — يتم تمرير البيانات عبر الاشتراك، مما يلغي الحاجة إلى واجهات ViewContract ويجعل الاختبار أبسط. توصي Apple بـ MVVM مع SwiftUI منذ 2019، وتوصي Google بـ MVVM مع Jetpack كمعمارية رسمية لنظام Android. المزيد في Android Architecture Guide.
الخلاصة
MVVM (Model-View-ViewModel) هو نمط معماري وصفه جون غوسمان في 2005 لـ Windows Presentation Foundation (WPF) من Microsoft. ViewModel هو المكون المركزي الذي يحتوي على حالة الشاشة ومنطق الأعمال ولكن ليس لديه مرجع إلى View. يتم تمرير البيانات عبر آليات الربط التفاعلي: تشترك View في تغييرات ViewModel وتُعاد عرضها تلقائياً عندما تتغير البيانات.
الفرق الرئيسي بين MVVM وMVP — غياب ViewContract. في MVP، يستدعي Presenter طرق view.showUser(data)، أي أن Presenter "يدفع" البيانات بنشاط إلى View. في MVVM، View نفسها "تسحب" البيانات من ViewModel عبر الاشتراك: لا يعرف ViewModel ما إذا كان لديه مشترك. هذا يلغي مشكلة View المنفصلة — إذا تم تدمير Activity عند التدوير، يستمر ViewModel في العمل، وتشترك Activity الجديدة ببساطة في البيانات الحالية. في IT Sectr، نستخدم MVVM في جميع المشاريع الجديدة منذ 2020 — أصبح الكود أكثر قابلية للتنبؤ، والاختبارات أكثر استقراراً.
| المكون | المسؤولية | المنصة |
|---|---|---|
| Model | البيانات، منطق الأعمال، المستودعات | Android/iOS |
| View | العرض، الاشتراك في ViewModel | Activity/Composable، UIView/SwiftUI View |
| ViewModel | حالة الشاشة، المنطق، التنقل | ViewModel (Jetpack)، ObservableObject |
الربط التفاعلي — أساس MVVM. في Android، LiveData (جزء من Jetpack) هو حامل بيانات قابل للملاحظة. تشترك Activity عبر observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. عندما يتغير user، يتلقى جميع المشتركين القيمة الجديدة تلقائياً. في iOS، يستخدم SwiftUI خصائص @Published في ViewModel — التغييرات تعيد عرض View تلقائياً. هذا يلغي استدعاءات showUser/hideLoading اليدوية المطلوبة في MVP.
ViewModel من Jetpack — المكون الرسمي من Google لتنفيذ MVVM. يتحمل ViewModel تدوير الشاشة: عندما يتغير التكوين، يتم تدمير Activity وإعادة إنشائها، بينما يبقى ViewModel في الذاكرة. تحصل مثيل Activity الجديد على نفس ViewModel عبر ViewModelProvider. لا يحتوي ViewModel على مراجع إلى Activity أو Context أو View — إنه نظيف ويُختبر باختبارات الوحدة بدون Robolectric.
// ViewModel مع StateFlow — تنفيذ حديث لـ MVVM
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View (Activity) تشترك في state
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* إظهار التحميل */
is UserState.Success -> /* عرض البيانات */
is UserState.Error -> /* إظهار الخطأ */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData مقابل StateFlow — LiveData (2017) — أول مكون تفاعلي في Jetpack، محسّن لدورة حياة Activity: إلغاء اشتراك تلقائي عند onStop. StateFlow (2021) — تنفيذ Kotlin Flow، غير مرتبط بدورة الحياة، ولكنه يتطلب إلغاء اشتراك يدوي عبر lifecycleScope. يدعم StateFlow coroutines وconcat وmap وعوامل Flow الأخرى، وهي غير متوفرة في LiveData. في IT Sectr، نستخدم StateFlow لجميع ViewModels الجديدة — إنه أقصر وأقوى ويتكامل بشكل أفضل مع coroutines.
DataBinding وViewBinding — يربط DataBinding ViewModel بـ XML عبر @{viewModel.user.name} مباشرة في التخطيط، مما يلغي الكود في Activity. يُنشئ ViewBinding فئة آمنة من حيث النوع للوصول إلى المشاهدات. توصي Google بـ ViewBinding للمشاريع البسيطة وDataBinding للمشاريع ذات الربط المعقد للبيانات. في Jetpack Compose، ليست هناك حاجة إلى DataBinding — دوال @Composable تُعاد عرضها تلقائياً عند تغير State.
MVVM في iOS يتم تنفيذه عبر ObservableObject من Combine. ViewModel هو فئة ترث ObservableObject، مع خصائص @Published. تشترك SwiftUI View في ViewModel عبر @ObservedObject أو @StateObject. عندما تتغير خاصية @Published، تعيد SwiftUI عرض View التي تعتمد على هذه الخاصية تلقائياً. قدمت Apple SwiftUI في 2019 في WWDC مع Combine — ومنذ ذلك الحين أصبح MVVM النمط الموصى به رسمياً لنظام iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject مع خصائص @Published
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View — تشترك في ViewModel
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject مقابل @ObservedObject — @StateObject ينشئ ViewModel ويدير دورة حياته (مرة واحدة طوال عمر View). @ObservedObject — يتم إنشاء ViewModel خارجياً وتمريره إلى View. توصي WWDC 2022 بـ @StateObject للإنشاء و@ObservedObject لتمرير ViewModel بين Views. في iOS 17 (2023)، ظهر macro @Observable — الذي يؤتمت الاشتراك ويلغي تعليقات @Published. @Observable هو تطور Combine، مما يقرب تطوير iOS من تفاعلية Kotlin Flow.
UIKit + MVVM — لمشاريع UIKit (بدون SwiftUI)، يتم تنفيذ MVVM عبر Combine و@Published مع الاشتراك في UIViewController عبر sink(). ViewModel هو نفسه، View هو UIViewController مع اشتراكات في @Published. Combine متاح منذ iOS 13 (2019) ومضمن في النظام — لا يتطلب تبعيات إضافية. وفقاً لـ Apple Developer Survey (2025)، 45% من مشاريع iOS تستخدم Combine حتى مع UIKit، و35% تستخدم SwiftUI + Combine، و20% تستخدم RxSwift (قديم).
MVVM يتفوق على MVP في ثلاثة جوانب رئيسية: غياب واجهات ViewContract، الإدارة التلقائية للاشتراكات، وتحمل تدوير الشاشة. في MVP، تتطلب كل شاشة واجهة ViewContract + فئة Presenter + اشتراك/إلغاء اشتراك في onStart/onStop. في MVVM، يتم إنشاء ViewModel فقط — يتم الاشتراك في Activity عبر observe() بدون detach() يدوي.
| المعيار | MVP | MVVM |
|---|---|---|
| واجهات ViewContract | 1 لكل شاشة | غير مطلوبة |
| إدارة الاشتراكات | attach/detach يدوي | تلقائي (lifecycle-aware) |
| تدوير الشاشة | Retain-fragment | ViewModel يتحمل التدوير |
| الاختبار | Mock ViewContract | فئة نظيفة بدون تبعيات |
| التفاعلية | استدعاءات في Presenter | LiveData/StateFlow/Combine |
عيوب MVVM — تعقيد تصحيح السلاسل التفاعلية وخطر تسرب الذاكرة مع الاشتراك غير الصحيح. LiveData يحل مشكلة أمان دورة الحياة، StateFlow يتطلب lifecycleScope، Combine يتطلب sink مع AnyCancellable. في MVP، كل الاستدعاءات صريحة (view.showUser)، في MVVM تأتي البيانات عبر تدفق تفاعلي — يتطلب التتبع نقاط توقف في إغلاقات subscribe. في ViewModels الكبيرة مع StateFlows متعددة، قد يفوتك تحديث UI إذا لم تكن View مشتركة في Flow معين.
عندما لا يزال MVP أفضل — في المشاريع ذات إصدار Android الأدنى أقل من API 21 (Android 5)، حيث ViewModel من Jetpack غير متاح بدون AndroidX، وفي المشاريع التي تستخدم UIKit النقي بدون Combine (iOS 12 وما دون). للمشاريع القديمة حيث قاعدة الكود بأكملها بالفعل على MVP، ليس الانتقال الكامل إلى MVVM مبرراً دائماً — من الأرخص الحفاظ على MVP مع استخراج تدريجي للمنطق في الخدمات بدلاً من إعادة كتابة 100 شاشة في 3 أشهر.
ViewModel يُختبر باختبارات الوحدة بدون تبعيات المنصة — هذه هي الحجة الرئيسية لصالح MVVM. في Android، لا يحتوي ViewModel على Activity أو Context أو View — جميع التبعيات (Repository، UseCase) تُمرر عبر المُنشئ وتُستبدل بأشياء mock. في iOS، يُختبر ObservableObject عبر XCTest بدون تشغيل التطبيق، مما يوفر الاستقرار وسرعة تنفيذ الاختبارات.
// اختبار وحدة ViewModel على Android باستخدام MockK
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
ViewModel على iOS يُختبر بشكل مشابه: نحقن mock UserService، نستدعي loadUser، نتحقق من الحالة عبر XCTestExpectation. يُختبر Combine Publisher عبر XCTestCase مع wait(for: expectations, timeout: 1.0). هيكل UserState — enum مع قيم مرتبطة — يسمح بالتحقق من حالة الشاشة بالضبط بعد العملية.
تغطية الكود في مشاريع IT Sectr التي تستخدم MVVM تبلغ 75–90% لـ ViewModel وRepository. ViewModel مغطى باختبارات الوحدة، Repository باختبارات التكامل مع قاعدة بيانات اختبارية. View في SwiftUI وJetpack Compose تُختبر باختبارات UI (XCUITest، Compose Test) للسيناريوهات الحرجة. باقي UI يُفحص باختبارات لقطات الشاشة (Snapshot Testing) — هذا أسرع من اختبارات UI ويعطي 95% ثقة في صحة العرض.
الأسئلة الشائعة
في MVVM، ليس لدى ViewModel مرجع إلى View — يتم تمرير البيانات عبر آليات تفاعلية (LiveData، StateFlow، @Published). في MVP، يستدعي Presenter طرق View مباشرة عبر واجهة ViewContract. MVVM يلغي ViewContract وattach/detach اليدوي، لكنه يتطلب فهم التدفقات التفاعلية. ViewModel يتحمل تدوير الشاشة في Android، Presenter يتطلب retain-fragment.
المجموعة الدنيا: lifecycle-viewmodel-ktx (ViewModel)، lifecycle-livedata-ktx أو kotlinx-coroutines-core (StateFlow). للحقن — Hilt أو Koin. للعمليات غير المتزامنة — Kotlin Coroutines. لربط البيانات المعقد — DataBinding. في Jetpack Compose (الموصى به من Google منذ 2022)، يكفي compose-runtime وlifecycle-viewmodel-compose.
SwiftUI (2019) مصمم للمعمارية التفاعلية: @State و@Published يعيدان عرض View تلقائياً عند تغير البيانات. MVVM مناسب طبيعياً لـ SwiftUI: View — @ViewBuilder، ViewModel — ObservableObject. Apple لا تفرض MVVM كنمط وحيد، لكن جميع المواد التعليمية منذ 2019 تستخدم ViewModel + SwiftUI. لـ UIKit، توصي Apple بـ MVC أو Coordinator.
Android: viewModelScope يلغي coroutines تلقائياً عند مسح ViewModel. iOS: AnyCancellable من Combine يلغي الاشتراك تلقائياً عند تحرير الكائن الحامل له. SwiftUI @StateObject يدير دورة الحياة تلقائياً. القواعد الرئيسية: لا تخزن مراجع إلى View/Context في ViewModel، ألغِ العمليات طويلة الأمد عند المسح، استخدم weak self في الإغلاقات.
StateFlow هو الخيار الحديث. LiveData أبسط وآمن لدورة الحياة، لكن StateFlow أقوى: يعمل مع coroutines، يدعم flatMap وcombine وfilter، لا يتطلب تعليق @Nullable. السيناريو الوحيد حيث LiveData أفضل — العمل مع كود Java حيث StateFlow (Kotlin Flow API) غير متاح. توصي Google بـ StateFlow للمشاريع الجديدة على Kotlin.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.