MVI — ماهیت الگوی Model-View-Intent در برنامه‌های موبایل

نویسنده: IT Sectr منتشر شده: 2026-02-16 زمان مطالعه: 10 دقیقه

MVI (Model-View-Intent) — یک الگوی معماری واکنش‌گرا مبتنی بر جریان داده یک‌طرفه و حالت تغییرناپذیر. برخلاف MVVM، که در آن ViewModel می‌تواند چندین StateFlow داشته باشد، MVI یک حالت واحد (State)، نیات تغییرناپذیر (Intent) و یک تابع reducer خالص (Reducer) تعریف می‌کند. MVI پیش‌بینی‌پذیری حالت صفحه را در هر لحظه تضمین می‌کند. این الگو توسط کتابخانه‌های Mosby و Orbit در جامعه Android محبوب شده است. بیشتر — در MVIKotlin از Arkadii Ivanov.

نکات اصلی

  • 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 — اشتراک در حالت و ارسال 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 → Statefun reduce(state, intent) -> state
Middlewareپردازش اثرات جانبیدرخواست شبکه، نوشتن در پایگاه داده

چرخه MVI شامل پنج مرحله است: 1) View یک Intent ارسال می‌کند (مثلاً LoadUser(42))؛ 2) Middleware (EffectHandler) effect جانبی را اجرا می‌کند — درخواست شبکه؛ 3) نتیجه به عنوان یک Intent جدید به سیستم بازمی‌گردد؛ 4) Reducer حالت فعلی و Intent را دریافت کرده، حالت جدیدی ایجاد می‌کند؛ 5) View حالت جدید را دریافت کرده و دوباره ترسیم می‌کند. هر مرحله پیش‌بینی‌پذیر و به صورت مجزا آزمایش می‌شود.

MVI در Android: Intent، Reducer، State در Kotlin

MVI در Android از طریق sealed-کلاس‌ها برای Intent و State، ViewModel با منطق MVI و Jetpack Compose برای نمایش واکنش‌گرا پیاده‌سازی می‌شود. ViewModel Intent را از View دریافت می‌کند، اثرات جانبی را به Middleware واگذار می‌کند، Reducer را اجرا کرده و حالت جدید را از طریق StateFlow منتشر می‌کند. Jetpack Compose با تغییر state 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 را پردازش می‌کند، 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: جریان یک‌طرفه در Swift

MVI در iOS بدون Combine-ViewModel، از طریق چرخه Intent → State پیاده‌سازی می‌شود. View از طریق closure Intent ارسال می‌کند، Reducer — تابع خالص، State — struct با فیلدهای تغییرناپذیر. SwiftUI با تغییر State View را دوباره ترسیم می‌کند که بدون ویژگی‌های اضافی @Published کاملاً در چرخه MVI قرار می‌گیرد. 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 فراتر رفته است — این استاندارد 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: چه زمانی MVI را انتخاب کنیم

MVI و MVVM یک任务 را حل می‌کنند — سازماندهی لایه Presentation — اما با رویکردهای متفاوت به مدیریت حالت. MVVM منابع واکنش‌گرای متعدد (LiveData، @Published) را مجاز می‌داند که می‌تواند منجر به ناسازگاری شود. MVI دقیقاً یک حالت را در هر لحظه تضمین می‌کند که آن را سخت‌گیرانه‌تر و پیش‌بینی‌پذیرتر می‌کند، اما حجم کد را افزایش می‌دهد.

معیارMVVMMVI
حالتچندین 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.

بهترین روش‌های MVI و اشتباهات رایج

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 با 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 لازم است؟

خیر — sealed Intent + sealed State + 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 و تابع reducer خالص پیاده‌سازی کرد. SwiftUI با State تغییرناپذیر کاملاً با چرخه MVI هماهنگ است.

چگونه MVI را آزمایش کنیم؟

Reducer با آزمایش‌های واحد به عنوان تابع خالص آزمایش می‌شود: State اولیه داده می‌شود، Intent ارسال می‌شود، State نهایی بررسی می‌شود. Middleware با مخزن ساختگی (mock) آزمایش می‌شود: بررسی می‌شود که پس از LoadUser، getUser فراخوانی شده است. آزمایش 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 از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید