MVI — Mobil Uygulamalarda Model-View-Intent Desenini Anlamak

Yazar: IT Sectr Yayınlanma: 2026-02-16 Okuma süresi: 10 dk

MVI (Model-View-Intent), tek yönlü veri akışı ve değişmez durum temelinde reaktif bir mimari desendir. Bir ViewModel'in birden çok StateFlow'a sahip olabileceği MVVM'nin aksine, MVI tek bir durum (State), değişmez niyetler (Intent) ve saf bir indirgeyici işlev (Reducer) tanımlar. MVI, herhangi bir zamanda ekran durumunun tahmin edilebilirliğini garanti eder. Desen, Mosby ve Orbit kütüphaneleri tarafından Android topluluğunda popüler hale getirilmiştir. Daha fazla bilgi için Arkadii Ivanov'un MVIKotlin'ine bakın.

Önemli Noktalar

  • MVI — Model (durum), View (görüntüleme), Intent (kullanıcı niyeti) — bir reaktif döngü
  • Unidirectional data flow — veri tek yönde akar: Intent → Reducer → State → View
  • Immutable State — ekran durumu değişmez bir nesnedir, her değişiklikte yeniden oluşturulur
  • Reducer — geçerli durumu ve Intent'i alıp yeni bir durum döndüren saf bir işlev
  • Side effects — yan etkiler (ağ, DB) Reducer'dan ayrı olarak Middleware aracılığıyla işlenir

MVI Nedir: Model-View-Intent Deseninin Özü

MVI (Model-View-Intent), Redux ve Cycle.js prensipleri üzerine inşa edilmiş reaktif bir mimari desendir. Model değişmez ekran durumudur, Intent kullanıcı veya sistem niyetidir, View durumu abone olur ve Intents gönderir. Veri bir döngüde akar: kullanıcı View ile etkileşime girer → View bir Intent oluşturur → Intent Reducer tarafından işlenir → Reducer yeni bir durum oluşturur → View yeni durumu alır ve yeniden oluşturur.

MVI ve MVVM arasındaki temel fark Tek Gerçek Kaynağıdır (Single Source of Truth). MVVM'de bir ViewModel birden çok LiveData/StateFlow'a (userState, loadingState, errorState) sahip olabilir, bu da tutarsızlığa yol açar: loading=true ve user=null aynı anda. MVI'de, tüm ekran durumunu tanımlayan tam olarak bir sealed class/interface State vardır. Herhangi bir zamanda, ekran durumu benzersiz şekilde belirlenir — veriler zaten yüklenmişken loading=true olması imkansızdır. IT Sectr'de, durum tahmin edilebilirliğinin kritik olduğu karmaşık mantığa sahip ekranlar — sipariş formları, çok adımlı kayıtlar, finansal ekranlar — için MVI uyguluyoruz.

BileşenMVI'deki RolüÖrnek
IntentKullanıcı veya sistem niyetiLoadUser, Refresh, SubmitForm
StateDeğişmez ekran durumusealed class UserState
ReducerSaf işlev: State + Intent → Statefun reduce(state, intent) -> state
MiddlewareYan etki işlemeAğ isteği, DB yazma

MVI döngüsü beş adımdan oluşur: 1) View bir Intent gönderir (ör. LoadUser(42)); 2) Middleware (EffectHandler) bir yan etki gerçekleştirir — bir ağ isteği; 3) Sonuç, sisteme yeni bir Intent olarak geri döndürülür; 4) Reducer geçerli durumu ve Intent'i alır, yeni bir durum oluşturur; 5) View yeni durumu alır ve yeniden oluşturur. Her adım tahmin edilebilir ve izole olarak test edilebilir.

Android'de MVI: Kotlin'de Intent, Reducer, State

Android'de MVI, Intent ve State için sealed sınıflar, MVI mantığına sahip bir ViewModel ve reaktif oluşturma için Jetpack Compose kullanılarak uygulanır. ViewModel, View'den Intent alır, yan etkileri Middleware'e devreder, Reducer'ı çalıştırır ve yeni durumu StateFlow aracılığıyla yayınlar. Jetpack Compose, durum değiştiğinde UI'yi yeniden oluşturur — MVI döngüsü için idealdir.

kotlin
// Intent — kullanıcı niyetleri
sealed interface UserIntent {
    data class LoadUser(val userId: Int) : UserIntent
    data object Refresh : UserIntent
}

// State — tek ekran durumu
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 — saf işlev
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// MVI ile ViewModel
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(/* önceki ID */)
        }
    }

    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 gönderir
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 ve yan etkiler — MVI'de saf bir Reducer ağ isteği gerçekleştiremez. Middleware (EffectHandler veya Bootstrapper olarak da adlandırılır) Intent'i işler, yan etkiyi gerçekleştirir ve döngüye yeni bir Intent gönderir. Orbit MVI ve MVIKotlin kütüphaneleri, test edilebilir efektlerle yerleşik Middleware desteği sağlar. Middleware olmadan MVI, ek Intent ve State yapısıyla MVVM'ye dönüşür.

Arkadii Ivanov'un MVIKotlin'i Kotlin Multiplatform için en popüler MVI kütüphanesidir. Android, iOS, web ve JVM'yi destekler. Store (ViewModel), Bootstrapper (başlangıç efektleri), Reducer, Middleware bileşenlerini sağlar. Ekim 2025 itibarıyla, kütüphane GitHub'da 2,5K yıldız toplamıştır ve büyük Rus bankalarının uygulamaları dahil ticari projelerde kullanılmaktadır. IT Sectr'de, paylaşılan iş mantığına sahip çapraz platform KMP projeleri için MVIKotlin kullanıyoruz.

iOS'ta MVI: Swift'te Tek Yönlü Akış

iOS'ta MVI, Combine-ViewModel olmadan, Intent → State döngüsü aracılığıyla uygulanır. View bir closure aracılığıyla Intent gönderir, Reducer saf bir işlevdir ve State değişmez alanlara sahip bir struct'tır. SwiftUI, State değiştiğinde View'i yeniden oluşturur ve ek @Published özellikleri olmadan MVI döngüsüne mükemmel uyum sağlar. iOS'ta MVI, özellikle Redux'tan (JavaScript) geçiş yapan SwiftUI geliştiricileri arasında popülerdir.

swift
// State — değişmez yapı
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

// Intent — niyetlerle enum
enum UserIntent {
    case loadUser(id: Int)
    case refresh
    case userLoaded(User)
    case loadFailed(Error)
}

// Reducer — saf işlev
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 — duruma sahiptir ve efektleri yönetir
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 durumu günceller
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (gerekirse)
        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), Point-Free tarafından iOS için en popüler MVI uygulamasıdır ve SwiftUI ile Combine üzerine inşa edilmiştir. TCA, Store, Reducer, Effect ve Environment sağlar. Ekim 2025 itibarıyla, GitHub yıldızları 13K'yı aşıyor — iOS'ta MVI için fiili standarttır. TCA, Starbucks uygulamalarında, Airbnb'de (kısmen) ve birçok bağımsız projede kullanılmaktadır. Özel MVI'nin aksine, TCA test, navigasyon ve yan etkileri kutu içinde çözer.

iOS'ta MVI vs MVVM — TCA/MVI durum tahmin edilebilirliği sağlar ancak daha fazla kalıp kod (Reducer, State, Action) gerektirir. @Published ile MVVM temel ekranlar için daha basittir. IT Sectr'de, ekranların %80'inde MVVM ve karmaşık olan %20'sinde — finansal işlemler, çok adımlı formlar, sürükle-bırak arayüzleri — MVI (TCA) kullanıyoruz. Burada bir durum hatası kullanıcıya para kaybettirebilir.

MVI vs MVVM: MVI Ne Zaman Seçilmeli

MVI ve MVVM aynı sorunu çözer — Sunum katmanını düzenlemek — ancak durum yönetimine farklı yaklaşımlarla. MVVM birden çok reaktif kaynağa (LiveData, @Published) izin verir, bu da tutarsızlığa yol açabilir. MVI herhangi bir anda tam olarak bir durumu garanti eder, bu da onu daha katı ve tahmin edilebilir kılar, ancak kod miktarını artırır.

KriterMVVMMVI
DurumBirden çok LiveData/StateFlowTek sealed class State
Veri akışıÇift yönlü (View → ViewModel, LiveData → View)Tek yönlü (Intent → Reducer → State → View)
Yan etkilerDoğrudan ViewModel'deMiddleware/EffectHandler aracılığıyla
TestViewModel birim testleriReducer + Middleware birim testleri
Kalıp kodMinimumReducer + State + Intent + Middleware

MVI ne zaman seçilmeli — durumun kesinlikle belirleyici olması gereken ekranlar: finansal işlemler, alışveriş sepeti, her adımda doğrulama içeren çok adımlı formlar. Bu senaryolarda, bir durum hatasının maliyeti (örneğin, iki LiveData arasındaki bir yarış koşulu nedeniyle bir öğe eksik sepet toplamı gösterme) ek kodun maliyetinden daha ağır basar. MVVM'de ekip disiplinine güvenirsiniz; MVI'de mimariye güvenirsiniz.

MVVM ne zaman yeterlidir — standart ekranların %80'i: kullanıcı listesi, profil, ayarlar, haber akışı. Burada tek bir durum gereksizdir ve ek MVI yapısı geliştirmeyi yavaşlatır. IT Sectr'de kural: bir ekranın geçişlerle (yükleme → veri → hata → yeniden dene → yükleme → veri) 3+ olası durumu varsa — MVI kullanın. Bir ekranın 1-2 asenkron işlemi varsa — MVVM kullanın.

MVI En İyi Uygulamaları ve Yaygın Hatalar

Sealed State — en iyi uygulama MVI'de. Durum, Loading, Success(data), Error(message) varyantlarıyla sealed class/interface olarak tanımlanır. Bu, View'in tutarsız bir duruma düşmemesini garanti eder — loading=true iken veri görüntülenemez çünkü Loading ve Success farklı sınıflardır. Durumla ilgili tüm veriler sealed varyantın içinde bulunur: Success kullanıcıyı içerir, Error hata mesajını içerir.

Reducer saf bir işlev olarak kalmalıdır — API, DB veya SharedPreferences çağrıları olmadan. Saf bir işlev State ve Intent'i alır ve State döndürür. Yan etkiler (ağ, DB, navigasyon, bildirimler) Middleware'de veya Reducer'ı çağırdıktan sonra Store.dispatch'te işlenir. Reducer yan etkilerle kirlenirse, MVI test edilebilirliğini ve tahmin edilebilirliğini kaybeder — avantajsız ek yapıya sahip MVVM elde edersiniz.

Yaygın hatalar — sealed class yerine nullable alanlara sahip data class olarak State bildirmek: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Bu MVVM'ye eşdeğerdir, MVI'ye değil — View geçerlilik için alan kombinasyonlarını kontrol etmelidir. Sealed yaklaşımında, geçersiz kombinasyonlar (isLoading=true ve user!=null) tür düzeyinde imkansızdır. İkinci hata, iş mantığını Middleware'e koymak yerine Intent'e (Intent.LoadUserBeforeXHours) koymaktır.

Sıkça Sorulan Sorular

MVI ve MVVM arasındaki temel fark nedir?

MVI, tek bir değişmez sealed State sınıfı ve Reducer aracılığıyla tek yönlü veri akışı kullanır. MVVM, çift yönlü bağlama ile birden çok LiveData/StateFlow'a izin verir. MVI, tür düzeyinde durum tutarlılığını garanti eder — loading=true ve user=null'un aynı anda olması imkansızdır. MVVM geliştirici disiplinine dayanır.

Android için hangi MVI kütüphaneleri var?

Başlıcaları: MVIKotlin (Arkadii Ivanov, 2,5K yıldız, Kotlin Multiplatform), Orbit MVI (BabyJ, 1,3K yıldız), Mobius (Spotify, Kotlin/Java). MVIKotlin Kotlin için en popüler, Orbit öğrenmesi en kolay. Üçü de test edilebilir Reducer ve Middleware'i destekler. Jetpack Compose için, sealed State + Reducer kullanarak kütüphane olmadan basit MVI yazabilirsiniz.

MVI için ayrı bir kütüphane gerekli mi?

Hayır — sealed Intent + sealed State + ViewModel + StateFlow bağımlılık olmadan çalışan MVI verir. Kütüphaneler (MVIKotlin, Orbit, TCA) Middleware, yan etki testi ve DI entegrasyonu ekler. Basit projeler için kütüphane ağırlığı haklı değildir. 20+ ekranlı karmaşık projeler için kütüphane, yapılandırılmış efekt işleme ile kendini amorti eder.

MVI iOS için uygun mu yoksa sadece Android deseni mi?

MVI, TCA (The Composable Architecture) — SwiftUI topluluğunun en popüler mimarisi — aracılığıyla iOS için harika çalışır. TCA esasen MVI + Redux + Combine'dır. iOS'ta, ObservableObject ve saf bir reducer işlevi kullanarak TCA olmadan MVI uygulayabilirsiniz. Değişmez State ile SwiftUI, MVI döngüsüne mükemmel uyum sağlar.

MVI nasıl test edilir?

Reducer, saf işlev olarak birim testleriyle test edilir: başlangıç State'ini ayarla, Intent gönder, sonuç State'ini kontrol et. Middleware, bir mock depo ile test edilir: LoadUser'dan sonra getUser'ın çağrıldığını doğrula. ViewModel testi: Intent gönder, StateFlow'u kontrol et. MVI, MVVM'den daha kolay test edilir çünkü Reducer gizli bağımlılıkları olmayan saf bir işlevdir.

Özet

  • MVI (Model-View-Intent) — tek yönlü akış ve tek durumlu reaktif bir desen
  • Sealed State — tür düzeyinde tutarlılığı garanti eder, geçersiz kombinasyonları ortadan kaldırır
  • Reducer — saf işlev State + Intent → State, mock nesneler olmadan test edilebilir
  • Middleware — yan etkiler (ağ, DB, navigasyon) için ayrı bir katman
  • MVI vs MVVM — MVI daha katı ve tahmin edilebilir, MVVM daha basit ve hızlı
  • Android — karmaşık ekranlar için MVIKotlin veya Orbit; basit için MVVM
  • iOS — TCA (The Composable Architecture) SwiftUI'de MVI standardıdır

Anahtar teslim bir mobil uygulama geliştireceğiz

IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.

Projeyi tartış

Ayrıca okuyun