MVI (Model-View-Intent) هو نمط معماري تفاعلي يعتمد على تدفق البيانات أحادي الاتجاه والحالة غير القابلة للتغيير. على عكس MVVM، حيث يمكن أن تحتوي ViewModel على عدة StateFlows، يحدد MVI حالة واحدة (State)، ونوايا غير قابلة للتغيير (Intent)، ودالة اختزال نقية (Reducer). يضمن MVI قابلية التنبؤ بحالة الشاشة في أي لحظة. تم نشر النمط في مجتمع Android بواسطة مكتبتي Mosby وOrbit. المزيد في MVIKotlin لأركادي إيفانوف.
الخلاصة
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 ← State | fun reduce(state, intent) -> state |
| Middleware | معالجة الآثار الجانبية | طلب شبكة، كتابة في قاعدة البيانات |
دورة MVI تتكون من خمس خطوات: 1) ترسل View Intent (مثلاً LoadUser(42))؛ 2) ينفذ Middleware (EffectHandler) أثرًا جانبيًا — طلب شبكة؛ 3) تُعاد النتيجة كـ Intent جديد إلى النظام؛ 4) يأخذ Reducer الحالة الحالية وIntent، وينشئ حالة جديدة؛ 5) تستلم View الحالة الجديدة وتُعيد التصيير. كل خطوة قابلة للتنبؤ وقابلة للاختبار بشكل منفصل.
MVI على Android يُنفذ باستخدام classes sealed لـ Intent وState، وViewModel بمنطق MVI، وJetpack Compose للتصيير التفاعلي. تستقبل ViewModel Intent من View، وتفوض الآثار الجانبية إلى Middleware، وتشغل Reducer، وتنشر الحالة الجديدة عبر StateFlow. يعيد Jetpack Compose تصيير UI عندما تتغير الحالة — مثالي لدورة MVI.
// 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 يُنفذ بدون Combine-ViewModel، عبر دورة Intent ← State. ترسل View Intent عبر إغلاق (closure)، Reducer دالة نقية، وState هي struct بحقول غير قابلة للتغيير. يعيد SwiftUI تصيير View عندما يتغير State، مما يتناسب تمامًا مع دورة MVI بدون خصائص @Published إضافية. MVI على iOS شائع بشكل خاص بين مطوري SwiftUI الذين انتقلوا من Redux (JavaScript).
// 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 يحلان نفس المشكلة — تنظيم طبقة العرض التقديمي — ولكن بأساليب مختلفة لإدارة الحالة. يسمح MVVM بمصادر تفاعلية متعددة (LiveData، @Published)، مما قد يؤدي إلى عدم الاتساق. يضمن MVI حالة واحدة بالضبط في أي لحظة، مما يجعله أكثر صرامة وقابلية للتنبؤ، لكنه يزيد من حجم الكود.
| المعيار | MVVM | MVI |
|---|---|---|
| الحالة | 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.
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 فئة State sealed واحدة غير قابلة للتغيير وتدفق بيانات أحادي الاتجاه عبر Reducer. يسمح MVVM بعدة LiveData/StateFlow مع ربط ثنائي الاتجاه. يضمن MVI اتساق الحالة على مستوى الأنواع — من المستحيل أن يكون loading=true وuser=null في نفس الوقت. يعتمد MVVM على انضباط المطور.
الرئيسية: 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.
لا — Intent sealed + State sealed + ViewModel + StateFlow يعطيك MVI عاملاً بدون تبعيات. تضيف المكتبات (MVIKotlin وOrbit وTCA) Middleware واختبار الآثار الجانبية والتكامل مع DI. للمشاريع البسيطة، وزن المكتبة غير مبرر. للمشاريع المعقدة التي تحتوي على 20+ شاشة، تُثبت المكتبة قيمتها بمعالجة منظمة للتأثيرات.
MVI يعمل بشكل رائع لـ iOS عبر TCA (The Composable Architecture) — الهندسة الأكثر شعبية في مجتمع SwiftUI. TCA هي في الأساس MVI + Redux + Combine. على iOS، يمكنك تنفيذ MVI بدون TCA باستخدام ObservableObject ودالة اختزال نقية. SwiftUI مع State غير القابل للتغيير يتناسب تمامًا مع دورة MVI.
يُختبر Reducer باختبارات وحدة كدالة نقية: تعيين State أولي، إرسال Intent، التحقق من State الناتج. يُختبر Middleware بمستودع وهمي (mock): التحقق من استدعاء getUser بعد LoadUser. اختبار ViewModel: إرسال Intent، التحقق من StateFlow. MVI أسهل في الاختبار من MVVM لأن Reducer دالة نقية بدون تبعيات مخفية.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.