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-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şen | MVI'deki Rolü | Örnek |
|---|---|---|
| Intent | Kullanıcı veya sistem niyeti | LoadUser, Refresh, SubmitForm |
| State | Değişmez ekran durumu | sealed class UserState |
| Reducer | Saf işlev: State + Intent → State | fun reduce(state, intent) -> state |
| Middleware | Yan etki işleme | Ağ 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, 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.
// 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, 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.
// 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 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.
| Kriter | MVVM | MVI |
|---|---|---|
| Durum | Birden çok LiveData/StateFlow | Tek sealed class State |
| Veri akışı | Çift yönlü (View → ViewModel, LiveData → View) | Tek yönlü (Intent → Reducer → State → View) |
| Yan etkiler | Doğrudan ViewModel'de | Middleware/EffectHandler aracılığıyla |
| Test | ViewModel birim testleri | Reducer + Middleware birim testleri |
| Kalıp kod | Minimum | Reducer + 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.
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, 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.
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.
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, 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.
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
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.
Ayrıca okuyun