MVVM: ما هو، نمط Model-View-ViewModel في تطوير التطبيقات المحمولة

المؤلف: IT Sectr نُشر: 2026-02-16 وقت القراءة: 9 دق

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 (حالة ومنطق بدون مرجع إلى View)
  • الربط التفاعلي — LiveData وStateFlow وObservableObject تحدث واجهة المستخدم تلقائياً عند تغير البيانات
  • ViewModel — يتحمل تدوير الشاشة ولا يعتمد على Android SDK/UIKit، يُختبر باختبارات الوحدة
  • Android Jetpack — ViewModel وLiveData وDataBinding — الحزمة الرسمية من Google لـ MVVM
  • SwiftUI + Combine — تنفيذ MVVM الأصلي في iOS باستخدام @Published و@ObservedObject

ما هو MVVM: جوهر نمط Model-View-ViewModel

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العرض، الاشتراك في ViewModelActivity/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.

MVVM في Android: ViewModel وLiveData وStateFlow

ViewModel من Jetpack — المكون الرسمي من Google لتنفيذ MVVM. يتحمل ViewModel تدوير الشاشة: عندما يتغير التكوين، يتم تدمير Activity وإعادة إنشائها، بينما يبقى ViewModel في الذاكرة. تحصل مثيل Activity الجديد على نفس ViewModel عبر ViewModelProvider. لا يحتوي ViewModel على مراجع إلى Activity أو Context أو View — إنه نظيف ويُختبر باختبارات الوحدة بدون Robolectric.

kotlin
// 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 وSwiftUI

MVVM في iOS يتم تنفيذه عبر ObservableObject من Combine. ViewModel هو فئة ترث ObservableObject، مع خصائص @Published. تشترك SwiftUI View في ViewModel عبر @ObservedObject أو @StateObject. عندما تتغير خاصية @Published، تعيد SwiftUI عرض View التي تعتمد على هذه الخاصية تلقائياً. قدمت Apple SwiftUI في 2019 في WWDC مع Combine — ومنذ ذلك الحين أصبح MVVM النمط الموصى به رسمياً لنظام iOS.

swift
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: المزايا والعيوب

MVVM يتفوق على MVP في ثلاثة جوانب رئيسية: غياب واجهات ViewContract، الإدارة التلقائية للاشتراكات، وتحمل تدوير الشاشة. في MVP، تتطلب كل شاشة واجهة ViewContract + فئة Presenter + اشتراك/إلغاء اشتراك في onStart/onStop. في MVVM، يتم إنشاء ViewModel فقط — يتم الاشتراك في Activity عبر observe() بدون detach() يدوي.

المعيارMVPMVVM
واجهات ViewContract1 لكل شاشةغير مطلوبة
إدارة الاشتراكاتattach/detach يدويتلقائي (lifecycle-aware)
تدوير الشاشةRetain-fragmentViewModel يتحمل التدوير
الاختبارMock ViewContractفئة نظيفة بدون تبعيات
التفاعليةاستدعاءات في PresenterLiveData/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 في Android وiOS

ViewModel يُختبر باختبارات الوحدة بدون تبعيات المنصة — هذه هي الحجة الرئيسية لصالح MVVM. في Android، لا يحتوي ViewModel على Activity أو Context أو View — جميع التبعيات (Repository، UseCase) تُمرر عبر المُنشئ وتُستبدل بأشياء mock. في iOS، يُختبر ObservableObject عبر XCTest بدون تشغيل التطبيق، مما يوفر الاستقرار وسرعة تنفيذ الاختبارات.

kotlin
// اختبار وحدة 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 وMVP؟

في MVVM، ليس لدى ViewModel مرجع إلى View — يتم تمرير البيانات عبر آليات تفاعلية (LiveData، StateFlow، @Published). في MVP، يستدعي Presenter طرق View مباشرة عبر واجهة ViewContract. MVVM يلغي ViewContract وattach/detach اليدوي، لكنه يتطلب فهم التدفقات التفاعلية. ViewModel يتحمل تدوير الشاشة في Android، Presenter يتطلب retain-fragment.

ما المكتبات اللازمة لـ MVVM على Android؟

المجموعة الدنيا: 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.

لماذا توصي Apple بـ MVVM لنظام iOS؟

SwiftUI (2019) مصمم للمعمارية التفاعلية: @State و@Published يعيدان عرض View تلقائياً عند تغير البيانات. MVVM مناسب طبيعياً لـ SwiftUI: View — @ViewBuilder، ViewModel — ObservableObject. Apple لا تفرض MVVM كنمط وحيد، لكن جميع المواد التعليمية منذ 2019 تستخدم ViewModel + SwiftUI. لـ UIKit، توصي Apple بـ MVC أو Coordinator.

كيف تتجنب تسرب الذاكرة في ViewModel؟

Android: viewModelScope يلغي coroutines تلقائياً عند مسح ViewModel. iOS: AnyCancellable من Combine يلغي الاشتراك تلقائياً عند تحرير الكائن الحامل له. SwiftUI @StateObject يدير دورة الحياة تلقائياً. القواعد الرئيسية: لا تخزن مراجع إلى View/Context في ViewModel، ألغِ العمليات طويلة الأمد عند المسح، استخدم weak self في الإغلاقات.

ماذا تختار: LiveData أم StateFlow لنظام Android؟

StateFlow هو الخيار الحديث. LiveData أبسط وآمن لدورة الحياة، لكن StateFlow أقوى: يعمل مع coroutines، يدعم flatMap وcombine وfilter، لا يتطلب تعليق @Nullable. السيناريو الوحيد حيث LiveData أفضل — العمل مع كود Java حيث StateFlow (Kotlin Flow API) غير متاح. توصي Google بـ StateFlow للمشاريع الجديدة على Kotlin.

الملخص

  • MVVM (Model-View-ViewModel) — نمط تفاعلي حيث ViewModel ليس لديه مرجع إلى View
  • ViewModel — يتحمل تدوير الشاشة، يُختبر باختبارات الوحدة، مستقل عن UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — حزمة حديثة من Google
  • iOS — ObservableObject + @Published + SwiftUI — تنفيذ MVVM أصلي
  • MVVM مقابل MVP — MVVM يلغي ViewContract وattach/detach اليدوي
  • الاختبار — ViewModel مغطى باختبارات الوحدة بدون تبعيات المنصة
  • التوصية — MVVM للمشاريع الجديدة؛ MVP لدعم القديم

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا