MVI — فهم نمط Model-View-Intent في تطبيقات الجوال

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

MVI (Model-View-Intent) هو نمط معماري تفاعلي يعتمد على تدفق البيانات أحادي الاتجاه والحالة غير القابلة للتغيير. على عكس MVVM، حيث يمكن أن تحتوي ViewModel على عدة StateFlows، يحدد MVI حالة واحدة (State)، ونوايا غير قابلة للتغيير (Intent)، ودالة اختزال نقية (Reducer). يضمن MVI قابلية التنبؤ بحالة الشاشة في أي لحظة. تم نشر النمط في مجتمع Android بواسطة مكتبتي Mosby وOrbit. المزيد في MVIKotlin لأركادي إيفانوف.

الخلاصة

  • MVI — Model (حالة)، View (عرض)، Intent (نية المستخدم) — دورة تفاعلية
  • Unidirectional data flow — تتحرك البيانات في اتجاه واحد: Intent ← Reducer ← State ← View
  • Immutable State — حالة الشاشة كائن غير قابل للتغيير، يُعاد إنشاؤه مع كل تغيير
  • Reducer — دالة نقية تأخذ الحالة الحالية وIntent، وتعيد حالة جديدة
  • Side effects — الآثار الجانبية (الشبكة، قاعدة البيانات) تُعالج بشكل منفصل عن Reducer عبر Middleware

ما هو MVI: جوهر نمط Model-View-Intent

MVI (Model-View-Intent) هو نمط معماري تفاعلي مبني على مبادئ Redux وCycle.js. Model هو حالة الشاشة غير القابلة للتغيير، Intent هو نية المستخدم أو النظام، View تشترك في الحالة وترسل Intents. تتدفق البيانات في دورة: يتفاعل المستخدم مع View ← تنشئ View Intent ← يعالج Reducer Intent ← ينشئ Reducer حالة جديدة ← تستلم View الحالة الجديدة وتُعيد التصيير.

الفرق الرئيسي بين MVI وMVVM هو المصدر الوحيد للحقيقة. في MVVM، يمكن أن تحتوي ViewModel على عدة LiveData/StateFlow (userState وloadingState وerrorState)، مما يؤدي إلى عدم الاتساق: loading=true وuser=null في نفس الوقت. في MVI، يوجد بالضبط class/interface sealed State واحد يصف حالة الشاشة بأكملها. في أي لحظة، يتم تحديد حالة الشاشة بشكل فريد — من المستحيل أن يكون loading=true بينما البيانات محملة بالفعل. في IT Sectr، نستخدم MVI للشاشات ذات المنطق المعقد — نماذج الطلب، التسجيلات متعددة الخطوات، الشاشات المالية — حيث تكون قابلية التنبؤ بالحالة أمرًا بالغ الأهمية.

المكونالدور في MVIمثال
Intentنية المستخدم أو النظامLoadUser, Refresh, SubmitForm
Stateحالة شاشة غير قابلة للتغييرsealed class UserState
Reducerدالة نقية: State + Intent ← Statefun reduce(state, intent) -> state
Middlewareمعالجة الآثار الجانبيةطلب شبكة، كتابة في قاعدة البيانات

دورة MVI تتكون من خمس خطوات: 1) ترسل View Intent (مثلاً LoadUser(42))؛ 2) ينفذ Middleware (EffectHandler) أثرًا جانبيًا — طلب شبكة؛ 3) تُعاد النتيجة كـ Intent جديد إلى النظام؛ 4) يأخذ Reducer الحالة الحالية وIntent، وينشئ حالة جديدة؛ 5) تستلم View الحالة الجديدة وتُعيد التصيير. كل خطوة قابلة للتنبؤ وقابلة للاختبار بشكل منفصل.

MVI في Android: Intent وReducer وState في Kotlin

MVI على Android يُنفذ باستخدام classes sealed لـ Intent وState، وViewModel بمنطق MVI، وJetpack Compose للتصيير التفاعلي. تستقبل ViewModel Intent من View، وتفوض الآثار الجانبية إلى Middleware، وتشغل Reducer، وتنشر الحالة الجديدة عبر StateFlow. يعيد Jetpack Compose تصيير UI عندما تتغير الحالة — مثالي لدورة MVI.

kotlin
// Intent — نوايا المستخدم
sealed interface UserIntent {
    data class LoadUser(val userId: Int) : UserIntent
    data object Refresh : UserIntent
}

// State — حالة الشاشة الوحيدة
sealed interface UserState {
    data object Idle : UserState
    data object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}

// Reducer — دالة نقية
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel مع MVI
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UserState>(UserState.Idle)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun process(intent: UserIntent) {
        val newState = UserReducer.reduce(_state.value, intent)
        _state.value = newState
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.userId)
            is UserIntent.Refresh -> loadUser(/* المعرف السابق */)
        }
    }

    private fun loadUser(userId: Int) {
        viewModelScope.launch {
            repository.getUser(userId)
                .onSuccess { user ->
                    _state.value = UserState.Success(user)
                }
                .onFailure { e ->
                    _state.value = UserState.Error(e.message ?: "Error")
                }
        }
    }
}

// View ترسل Intent
fun UserScreen(viewModel: UserViewModel) {
    val state by viewModel.state.collectAsState()
    LaunchedEffect(Unit) { viewModel.process(UserIntent.LoadUser(42)) }
    when (state) {
        is UserState.Loading -> CircularProgressIndicator()
        is UserState.Success -> UserCard((state as UserState.Success).user)
        is UserState.Error -> ErrorView((state as UserState.Error).message)
        is UserState.Idle -> Text("Press to load")
    }
}

Middleware والآثار الجانبية — في MVI، لا يمكن لـ Reducer النقي تنفيذ طلبات الشبكة. يعالج Middleware (يُسمى أيضًا EffectHandler أو Bootstrapper) Intent، وينفذ الأثر الجانبي، ويصدر Intent جديدًا مرة أخرى إلى الدورة. توفر مكتبتا Orbit MVI وMVIKotlin دعمًا مدمجًا لـ Middleware مع تأثيرات قابلة للاختبار. بدون Middleware، يتدهور MVI إلى MVVM مع هيكل إضافي لـ Intent وState.

MVIKotlin لأركادي إيفانوف هي مكتبة MVI الأكثر شهرة لـ Kotlin Multiplatform. تدعم Android وiOS والويب وJVM. توفر المكونات: Store (ViewModel)، Bootstrapper (التأثيرات الأولية)، Reducer، Middleware. اعتبارًا من أكتوبر 2025، حققت المكتبة 2.5K نجمة على GitHub وتُستخدم في مشاريع تجارية، بما في ذلك تطبيقات البنوك الروسية الكبرى. في IT Sectr، نستخدم MVIKotlin للمشاريع عبر المنصات KMP بمنطق أعمال مشترك.

MVI في iOS: التدفق أحادي الاتجاه في Swift

MVI على iOS يُنفذ بدون Combine-ViewModel، عبر دورة Intent ← State. ترسل View Intent عبر إغلاق (closure)، Reducer دالة نقية، وState هي struct بحقول غير قابلة للتغيير. يعيد SwiftUI تصيير View عندما يتغير State، مما يتناسب تمامًا مع دورة MVI بدون خصائص @Published إضافية. MVI على iOS شائع بشكل خاص بين مطوري SwiftUI الذين انتقلوا من Redux (JavaScript).

swift
// State — هيكل غير قابل للتغيير
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

// Intent — enum مع النوايا
enum UserIntent {
    case loadUser(id: Int)
    case refresh
    case userLoaded(User)
    case loadFailed(Error)
}

// Reducer — دالة نقية
func userReducer(state: UserState, intent: UserIntent) -> UserState {
    var newState = state
    switch intent {
    case .loadUser, .refresh:
        newState.isLoading = true
        newState.errorMessage = nil
    case .userLoaded(let user):
        newState.isLoading = false
        newState.user = user
    case .loadFailed(let error):
        newState.isLoading = false
        newState.errorMessage = error.localizedDescription
    }
    return newState
}

// Store — يملك الحالة ويدير التأثيرات
final class UserStore: ObservableObject {
    @Published private(set) var state = UserState()
    private let service: UserService

    init(service: UserService) {
        self.service = service
    }

    func dispatch(_ intent: UserIntent) {
        // 1. Reducer يحدث الحالة
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (إذا لزم الأمر)
        switch intent {
        case .loadUser(let id), .refresh:
            service.fetchUser(id: id) { [weak self] result in
                switch result {
                case .success(let user):
                    self?.dispatch(.userLoaded(user))
                case .failure(let error):
                    self?.dispatch(.loadFailed(error))
                }
            }
        default: break
        }
    }
}

TCA (The Composable Architecture) هي أشهر تطبيق MVI لنظام iOS من Point-Free، المبنية على SwiftUI وCombine. توفر TCA Store وReducer وEffect وEnvironment. اعتبارًا من أكتوبر 2025، تجاوزت نجومها على GitHub 13K — وهو المعيار الفعلي لـ MVI على iOS. تُستخدم TCA في تطبيقات Starbucks وAirbnb (جزئيًا) والعديد من المشاريع المستقلة. على عكس MVI المخصص، تعالج TCA الاختبارات والتنقل والآثار الجانبية مباشرةً.

MVI مقابل MVVM على iOS — توفر TCA/MVI قابلية التنبؤ بالحالة ولكنها تتطلب كودًا نموذجيًا أكثر (Reducer وState وAction). MVVM مع @Published أبسط للشاشات الأساسية. في IT Sectr، نستخدم MVVM لـ 80% من الشاشات وMVI (TCA) لـ 20% المعقدة — المعاملات المالية، النماذج متعددة الخطوات، واجهات السحب والإفلات، حيث يمكن أن يكلف خطأ الحالة المستخدم المال.

مقارنة MVI مع MVVM: متى تختار MVI

MVI وMVVM يحلان نفس المشكلة — تنظيم طبقة العرض التقديمي — ولكن بأساليب مختلفة لإدارة الحالة. يسمح MVVM بمصادر تفاعلية متعددة (LiveData، @Published)، مما قد يؤدي إلى عدم الاتساق. يضمن MVI حالة واحدة بالضبط في أي لحظة، مما يجعله أكثر صرامة وقابلية للتنبؤ، لكنه يزيد من حجم الكود.

المعيارMVVMMVI
الحالةLiveData/StateFlow متعددةclass State sealed واحد
تدفق البياناتثنائي الاتجاه (View → ViewModel، LiveData → View)أحادي الاتجاه (Intent → Reducer → State → View)
الآثار الجانبيةمباشرة في ViewModelعبر Middleware/EffectHandler
الاختباراختبارات وحدة لـ ViewModelاختبارات وحدة لـ Reducer + Middleware
الكود النموذجيأدنىReducer + State + Intent + Middleware

متى تختار MVI — الشاشات حيث يجب أن تكون الحالة حتمية تمامًا: العمليات المالية، سلة التسوق، النماذج متعددة الخطوات مع التحقق في كل خطوة. في هذه السيناريوهات، فإن تكلفة خطأ الحالة (مثل إظهار إجمالي السلة بدون عنصر واحد بسبب سباق بين LiveData) تفوق تكلفة الكود الإضافي. في MVVM تعتمد على انضباط الفريق؛ في MVI تعتمد على الهندسة المعمارية.

متى يكون MVVM كافيًا — 80% من الشاشات القياسية: قائمة المستخدمين، الملف الشخصي، الإعدادات، خلاصة الأخبار. هنا، الحالة الواحدة مبالغ فيها، والهيكل الإضافي لـ MVI سيُبطئ التطوير. في IT Sectr، القاعدة هي: إذا كانت الشاشة تحتوي على 3+ حالات محتملة مع انتقالات (تحميل ← بيانات ← خطأ ← إعادة محاولة ← تحميل ← بيانات) — استخدم MVI. إذا كانت الشاشة تحتوي على 1-2 عملية غير متزامنة — استخدم MVVM.

أفضل ممارسات MVI والأخطاء الشائعة

Sealed State — أفضل ممارسة في MVI. يتم تعريف الحالة كـ class/interface sealed مع متغيرات: Loading وSuccess(data) وError(message). يضمن ذلك أن View لن تنتهي في حالة غير متناسقة — لا يمكن عرض البيانات عندما يكون loading=true لأن Loading وSuccess فئتان مختلفتان. جميع البيانات المتعلقة بالحالة موجودة داخل المتغير sealed: Success تحتوي على المستخدم، Error تحتوي على رسالة الخطأ.

يجب أن يظل Reducer دالة نقية — بدون استدعاءات API أو قاعدة بيانات أو SharedPreferences. تأخذ الدالة النقية State وIntent وتعيد State. تُعالج الآثار الجانبية (الشبكة، قاعدة البيانات، التنقل، الإشعارات) في Middleware أو في Store.dispatch بعد استدعاء Reducer. إذا تلوث Reducer بالآثار الجانبية، يفقد MVI قابلية الاختبار والتنبؤ — تحصل على MVVM بهيكل إضافي بدون فوائد.

الأخطاء الشائعة — تعريف State كـ data class بحقول nullable بدلاً من sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). هذا يعادل MVVM، وليس MVI — يجب على View التحقق من تركيبات الحقول للصلاحية. في النهج sealed، التركيبات غير الصالحة (isLoading=true وuser!=null) مستحيلة على مستوى الأنواع. الخطأ الثاني هو وضع منطق الأعمال في Intent (Intent.LoadUserBeforeXHours) بدلاً من إنشاء Intents أوامر بسيطة (Intent.LoadUser) ووضع منطق الأعمال في Middleware.

الأسئلة الشائعة

ما الفرق الرئيسي بين MVI وMVVM؟

يستخدم MVI فئة State sealed واحدة غير قابلة للتغيير وتدفق بيانات أحادي الاتجاه عبر Reducer. يسمح MVVM بعدة LiveData/StateFlow مع ربط ثنائي الاتجاه. يضمن MVI اتساق الحالة على مستوى الأنواع — من المستحيل أن يكون loading=true وuser=null في نفس الوقت. يعتمد MVVM على انضباط المطور.

ما هي مكتبات MVI الموجودة لـ Android؟

الرئيسية: MVIKotlin (Arkadii Ivanov، 2.5K نجمة، Kotlin Multiplatform)، Orbit MVI (BabyJ، 1.3K نجمة)، Mobius (Spotify، Kotlin/Java). MVIKotlin هي الأكثر شهرة لـ Kotlin، Orbit هي الأسهل للتعلم. جميعها تدعم Reducer وMiddleware قابلين للاختبار. لـ Jetpack Compose، يمكنك كتابة MVI بسيط بدون مكتبة باستخدام sealed State + Reducer.

هل هناك حاجة لمكتبة منفصلة لـ MVI؟

لا — Intent sealed + State sealed + ViewModel + StateFlow يعطيك MVI عاملاً بدون تبعيات. تضيف المكتبات (MVIKotlin وOrbit وTCA) Middleware واختبار الآثار الجانبية والتكامل مع DI. للمشاريع البسيطة، وزن المكتبة غير مبرر. للمشاريع المعقدة التي تحتوي على 20+ شاشة، تُثبت المكتبة قيمتها بمعالجة منظمة للتأثيرات.

هل MVI مناسب لـ iOS أم هو نمط Android فقط؟

MVI يعمل بشكل رائع لـ iOS عبر TCA (The Composable Architecture) — الهندسة الأكثر شعبية في مجتمع SwiftUI. TCA هي في الأساس MVI + Redux + Combine. على iOS، يمكنك تنفيذ MVI بدون TCA باستخدام ObservableObject ودالة اختزال نقية. SwiftUI مع State غير القابل للتغيير يتناسب تمامًا مع دورة MVI.

كيف تختبر MVI؟

يُختبر Reducer باختبارات وحدة كدالة نقية: تعيين State أولي، إرسال Intent، التحقق من State الناتج. يُختبر Middleware بمستودع وهمي (mock): التحقق من استدعاء getUser بعد LoadUser. اختبار ViewModel: إرسال Intent، التحقق من StateFlow. MVI أسهل في الاختبار من MVVM لأن Reducer دالة نقية بدون تبعيات مخفية.

الملخص

  • MVI (Model-View-Intent) — نمط تفاعلي مع تدفق أحادي الاتجاه وحالة واحدة
  • Sealed State — يضمن الاتساق على مستوى الأنواع، مستبعدًا التركيبات غير الصالحة
  • Reducer — دالة نقية State + Intent ← State، قابلة للاختبار بدون كائنات وهمية
  • Middleware — طبقة منفصلة للآثار الجانبية (الشبكة، قاعدة البيانات، التنقل)
  • MVI vs MVVM — MVI أكثر صرامة وقابلية للتنبؤ، MVVM أبسط وأسرع
  • Android — MVIKotlin أو Orbit للشاشات المعقدة؛ MVVM للبسيطة
  • iOS — TCA (The Composable Architecture) هو معيار MVI على SwiftUI

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

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

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

اقرأ أيضًا