MVI (Model-View-Intent) — یک الگوی معماری واکنشگرا مبتنی بر جریان داده یکطرفه و حالت تغییرناپذیر. برخلاف MVVM، که در آن ViewModel میتواند چندین StateFlow داشته باشد، MVI یک حالت واحد (State)، نیات تغییرناپذیر (Intent) و یک تابع reducer خالص (Reducer) تعریف میکند. MVI پیشبینیپذیری حالت صفحه را در هر لحظه تضمین میکند. این الگو توسط کتابخانههای Mosby و Orbit در جامعه Android محبوب شده است. بیشتر — در MVIKotlin از Arkadii Ivanov.
نکات اصلی
MVI (Model-View-Intent) — یک الگوی معماری واکنشگرا ساخته شده بر اصول Redux و Cycle.js. Model — حالت تغییرناپذیر صفحه، Intent — نیت کاربر یا سیستم، View — اشتراک در حالت و ارسال Intent. دادهها در چرخه حرکت میکنند: کاربر با View تعامل میکند → View یک Intent ایجاد میکند → Intent توسط Reducer پردازش میشود → Reducer حالت جدیدی ایجاد میکند → View حالت جدید را دریافت کرده و دوباره ترسیم میکند.
تفاوت اصلی MVI با MVVM — منبع واحد حقیقت (Single Source of Truth). در MVVM، ViewModel میتواند چندین LiveData/StateFlow (userState، loadingState، errorState) داشته باشد که منجر به ناسازگاری میشود: loading=true و user=null به طور همزمان. در MVI دقیقاً یک sealed class/interface 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) effect جانبی را اجرا میکند — درخواست شبکه؛ 3) نتیجه به عنوان یک Intent جدید به سیستم بازمیگردد؛ 4) Reducer حالت فعلی و Intent را دریافت کرده، حالت جدیدی ایجاد میکند؛ 5) View حالت جدید را دریافت کرده و دوباره ترسیم میکند. هر مرحله پیشبینیپذیر و به صورت مجزا آزمایش میشود.
MVI در Android از طریق sealed-کلاسها برای Intent و State، ViewModel با منطق MVI و Jetpack Compose برای نمایش واکنشگرا پیادهسازی میشود. ViewModel Intent را از View دریافت میکند، اثرات جانبی را به Middleware واگذار میکند، Reducer را اجرا کرده و حالت جدید را از طریق StateFlow منتشر میکند. Jetpack Compose با تغییر state 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 را پردازش میکند، effect جانبی را اجرا کرده و Intent جدیدی را به چرخه بازمیگرداند. کتابخانههای Orbit MVI و MVIKotlin پشتیبانی داخلی از Middleware با اثرات قابل آزمایش ارائه میدهند. بدون Middleware، MVI به MVVM با ساختار اضافی Intent و State تبدیل میشود.
MVIKotlin از Arkadii Ivanov — محبوبترین کتابخانه MVI برای Kotlin Multiplatform. از Android، iOS، web و JVM پشتیبانی میکند. مؤلفههای Store (ViewModel)، Bootstrapper (اثرات اولیه)، Reducer، Middleware را فراهم میکند. تا اکتبر 2025، این کتابخانه 2,5K ستاره در GitHub جمع آوری کرده و در پروژههای تجاری از جمله برنامههای بانکهای بزرگ روسیه استفاده میشود. در IT Sectr ما از MVIKotlin برای پروژههای کراس-پلتفرم KMP با منطق تجاری مشترک استفاده میکنیم.
MVI در iOS بدون Combine-ViewModel، از طریق چرخه Intent → State پیادهسازی میشود. View از طریق closure Intent ارسال میکند، Reducer — تابع خالص، State — struct با فیلدهای تغییرناپذیر. SwiftUI با تغییر State View را دوباره ترسیم میکند که بدون ویژگیهای اضافی @Published کاملاً در چرخه MVI قرار میگیرد. 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 فراتر رفته است — این استاندارد de facto برای MVI در iOS است. TCA در برنامههای Starbucks، Airbnb (بخشی) و بسیاری از پروژههای مستقل استفاده میشود. برخلاف MVI دستنویس، TCA مسائل آزمایش، ناوبری و اثرات جانبی را به صورت آماده حل میکند.
MVI در مقابل MVVM در iOS — TCA/MVI پیشبینیپذیری حالت را فراهم میکند، اما کد قالبی بیشتری نیاز دارد (Reducer، State، Action). MVVM با @Published برای صفحات ساده آسانتر است. ما در IT Sectr از MVVM برای 80% صفحات و از MVI (TCA) برای 20% صفحات پیچیده استفاده میکنیم — تراکنشهای مالی، فرمهای چندمرحلهای، رابطهای drag-and-drop، جایی که خطای حالت ممکن است برای کاربر هزینه داشته باشد.
MVI و MVVM یک任务 را حل میکنند — سازماندهی لایه Presentation — اما با رویکردهای متفاوت به مدیریت حالت. MVVM منابع واکنشگرای متعدد (LiveData، @Published) را مجاز میداند که میتواند منجر به ناسازگاری شود. MVI دقیقاً یک حالت را در هر لحظه تضمین میکند که آن را سختگیرانهتر و پیشبینیپذیرتر میکند، اما حجم کد را افزایش میدهد.
| معیار | MVVM | MVI |
|---|---|---|
| حالت | چندین LiveData/StateFlow | یک sealed class State واحد |
| جریان داده | دوطرفه (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. حالت به عنوان sealed class/interface با گزینههای Loading، Success(data)، Error(message) تعریف میشود. این تضمین میکند که View وارد حالت ناسازگار نشود — نمیتوان دادهها را با loading=true نمایش داد، زیرا Loading و Success کلاسهای متفاوتی هستند. تمام دادههای مربوط به حالت درون variant sealed قرار دارند: Success شامل کاربر، Error — پیام خطا.
Reducer باید یک تابع خالص باقی بماند — بدون فراخوانی API، پایگاه داده، SharedPreferences. تابع خالص State و Intent را دریافت کرده، State را برمیگرداند. اثرات جانبی (شبکه، پایگاه داده، ناوبری، toastها) در 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) به جای ایجاد Intentهای فرمانی ساده (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 بنویسید.
خیر — sealed Intent + sealed State + ViewModel + StateFlow یک MVI کارآمد بدون وابستگی ایجاد میکنند. کتابخانهها (MVIKotlin، Orbit، TCA) Middleware، آزمایش اثرات جانبی و یکپارچهسازی با DI را اضافه میکنند. برای پروژههای ساده، وزن کتابخانه توجیهپذیر نیست. برای پروژههای پیچیده با 20+ صفحه، کتابخانه با پردازش ساختاریافته اثرات خود را توجیه میکند.
MVI کاملاً برای iOS از طریق TCA (The Composable Architecture) — محبوبترین معماری جامعه SwiftUI — مناسب است. TCA در واقع MVI + Redux + Combine است. در iOS میتوان MVI را بدون TCA نیز از طریق ObservableObject و تابع reducer خالص پیادهسازی کرد. SwiftUI با State تغییرناپذیر کاملاً با چرخه MVI هماهنگ است.
Reducer با آزمایشهای واحد به عنوان تابع خالص آزمایش میشود: State اولیه داده میشود، Intent ارسال میشود، State نهایی بررسی میشود. Middleware با مخزن ساختگی (mock) آزمایش میشود: بررسی میشود که پس از LoadUser، getUser فراخوانی شده است. آزمایش ViewModel: ارسال Intent، بررسی StateFlow. MVI راحتتر از MVVM آزمایش میشود، زیرا Reducer یک تابع خالص بدون وابستگیهای پنهان است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید