Android'de MVVM vs MVI: 2026'da Hangi Mimariyi Seçmeli?
Android'de MVVM ve MVI karşılaştırması: avantajlar, kısıtlamalar, kullanım senaryoları ve 2026'da doğru mimariyi seçmek için pratik rehber.

Doğru mimariyi seçmek, bir Android uygulamasının sürdürülebilirliğini, test edilebilirliğini ve ölçeklenebilirliğini doğrudan etkileyen kritik bir karardır. 2026'da iki desen ekosisteme hükmetmektedir: endüstri standardı MVVM ve Jetpack Compose ile birlikte ivme kazanan reaktif yaklaşım MVI.
Yanlış bir mimari seçimi pahalıya mal olur: teknik borç, yeniden üretilmesi zor hatalar ve acı veren yeniden yapılandırmalar. Her yaklaşımın güçlü ve zayıf yönlerini anlamak, uzun vadede büyük baş ağrılarını önler.
MVVM'yi Anlamak: Yerleşik Standart
MVVM (Model-View-ViewModel), Jetpack'in tanıtılmasından bu yana Google'ın önerdiği mimaridir. Sorumlulukları üç farklı katmana temiz biçimde ayırarak kodu daha düzenli ve test edilebilir hale getirir.
MVVM'nin Temel İlkeleri
MVVM deseni açık bir ayrışmaya dayanır: Model veri ve iş mantığını yönetir, View arayüzü gösterir, ViewModel ise gözlemlenebilir durumlar sunarak ikisini birbirine bağlar.
Aşağıda MVVM ile bir kullanıcı profili ekranı uygulanmaktadır. Bu ilk örnek, gözlemlenebilir durum ve kullanıcı etkileşim yöntemleri sunan ViewModel'in temel yapısını göstermektedir.
// Classic MVVM ViewModel for a user profile screen
// It exposes observable state and methods for actions
class UserProfileViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Observable state with StateFlow - the View observes these changes
private val _uiState = MutableStateFlow(UserProfileState())
val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()
// Separate loading state - MVVM allows multiple flows
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// One-shot error messages
private val _errorMessage = MutableSharedFlow<String>()
val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()
// Initial profile loading
fun loadProfile(userId: String) {
viewModelScope.launch {
_isLoading.value = true
try {
// Repository call to fetch data
val user = userRepository.getUser(userId)
// Update state with new data
_uiState.update { currentState ->
currentState.copy(
user = user,
isEditing = false
)
}
// Analytics tracking
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Emit one-shot error message
_errorMessage.emit("Unable to load profile")
} finally {
_isLoading.value = false
}
}
}
// Enable edit mode
fun enableEditMode() {
_uiState.update { it.copy(isEditing = true) }
}
// Save profile changes
fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
_isLoading.value = true
try {
val updatedUser = userRepository.updateUser(
_uiState.value.user?.id ?: return@launch,
name = name,
bio = bio
)
_uiState.update {
it.copy(user = updatedUser, isEditing = false)
}
} catch (e: Exception) {
_errorMessage.emit("Failed to save profile")
} finally {
_isLoading.value = false
}
}
}
}
// Data class representing the screen state
data class UserProfileState(
val user: User? = null,
val isEditing: Boolean = false
)Bu ViewModel tipik MVVM yaklaşımını örneklemektedir: birden fazla gözlemlenebilir flow (ana durum, yükleme, hatalar) ve her kullanıcı eylemi için genel metotlar.
MVVM'nin Avantajları
MVVM'nin yaygın benimsenmesini açıklayan birçok güçlü yönü vardır:
- Tanışıklık: Android geliştiricilerin çoğu bu deseni bilir
- Esneklik: Durum yapısı tamamen serbesttir
- Ekosistem: Jetpack ile mükemmel entegrasyon (LiveData, StateFlow, Hilt)
- Basitlik: Yeni başlayanlar için düşük öğrenme eğrisi
MVVM, farklı deneyim seviyelerindeki geliştiricilerden oluşan karma ekipler için özellikle uygundur. Kavramsal sadeliği işe alım sürecini kolaylaştırır.
MVVM'nin Kısıtlamaları
Ancak MVVM, uygulama büyüdükçe kısıtlamalarını gösterir. Temel sorun dağıtık durum yönetimidir. Aşağıdaki örnek bu yaygın sorunu göstermektedir:
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {
// Problem: state scattered across multiple flows
private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
private val _selectedAddress = MutableStateFlow<Address?>(null)
private val _selectedPayment = MutableStateFlow<PaymentMethod?>(null)
private val _promoCode = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _error = MutableStateFlow<String?>(null)
// Each modification can create temporary inconsistent states
fun applyPromoCode(code: String) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null // Reset error
try {
val discount = promoRepository.validate(code)
_promoCode.value = code
// Cart state also needs updating...
// but there's a delay between the two updates
recalculateCart()
} catch (e: Exception) {
_error.value = e.message
_promoCode.value = null
} finally {
_isLoading.value = false
}
}
}
// Hard to guarantee consistency across all these states
private fun recalculateCart() {
// Complex logic depending on multiple states...
}
}Bu örnek, MVVM'de durumun nasıl parçalanabileceğini ve geçişleri izlemeyi ile hataları yeniden üretmeyi nasıl zorlaştırdığını göstermektedir.
MVI'yi Anlamak: Tek Yönlü Yaklaşım
MVI (Model-View-Intent) farklı bir felsefe benimser: tek yönlü veri akışı ve tek değişmez durum. Redux'tan ilham alan bu yaklaşım, tutarsız durum sorunlarını ortadan kaldırır.
MVI'nin Temel İlkeleri
MVI'de her şey açık bir döngüyü izler: kullanıcı bir Intent (eylem) gönderir, Reducer mevcut durumu yeni bir duruma dönüştürür, View ise bu tek durumu gösterir. Öngörülebilir, test edilebilir ve hata ayıklanabilirdir.
Aşağıda aynı kullanıcı profili ekranı MVI ile uygulanmaktadır. Durumun nasıl merkezi hale getirildiğine ve eylemlerin nasıl açıkça tiplendiğine dikkat edilmelidir.
// MVI ViewModel for the same user profile screen
// Note the structure: Intent → Reducer → Single State
class UserProfileMviViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Single, immutable state - the absolute source of truth
private val _state = MutableStateFlow(UserProfileState())
val state: StateFlow<UserProfileState> = _state.asStateFlow()
// Channel for side effects (navigation, snackbar)
private val _sideEffect = Channel<UserProfileSideEffect>()
val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()
// Single entry point for all user actions
fun onIntent(intent: UserProfileIntent) {
when (intent) {
is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
is UserProfileIntent.EnableEditMode -> enableEditMode()
is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
is UserProfileIntent.CancelEdit -> cancelEdit()
}
}
private fun loadProfile(userId: String) {
viewModelScope.launch {
// Transition to loading state
_state.update { it.copy(isLoading = true, error = null) }
try {
val user = userRepository.getUser(userId)
// Single atomic state update
_state.update {
it.copy(
user = user,
isLoading = false,
error = null
)
}
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Error state is part of the main state
_state.update {
it.copy(
isLoading = false,
error = "Unable to load profile"
)
}
}
}
}
private fun enableEditMode() {
// Simple, predictable update
_state.update { it.copy(isEditing = true) }
}
private fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
val currentUser = _state.value.user ?: return@launch
_state.update { it.copy(isLoading = true) }
try {
val updatedUser = userRepository.updateUser(
currentUser.id,
name = name,
bio = bio
)
_state.update {
it.copy(
user = updatedUser,
isEditing = false,
isLoading = false
)
}
// Side effect to notify the user
_sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))
} catch (e: Exception) {
_state.update {
it.copy(isLoading = false, error = "Failed to save profile")
}
}
}
}
private fun cancelEdit() {
_state.update { it.copy(isEditing = false) }
}
}
// All possible actions, explicitly typed
sealed class UserProfileIntent {
data class LoadProfile(val userId: String) : UserProfileIntent()
object EnableEditMode : UserProfileIntent()
data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
object CancelEdit : UserProfileIntent()
}
// Single, complete screen state
data class UserProfileState(
val user: User? = null,
val isLoading: Boolean = false,
val isEditing: Boolean = false,
val error: String? = null
)
// One-shot side effects
sealed class UserProfileSideEffect {
data class ShowSuccess(val message: String) : UserProfileSideEffect()
data class NavigateTo(val destination: String) : UserProfileSideEffect()
}Fark açıktır: tek bir durum flow'u, açık eylemler ve kalıcı durum ile tek seferlik etkiler arasında temiz bir ayrım.
MVI ile her Intent ve her durum geçişi günlüğe kaydedilebilir. Bir hatayı yeniden üretmek önemsiz hale gelir: Intent dizisini tekrar oynatmak yeterlidir.
Jetpack Compose ile MVI
MVI özellikle Jetpack Compose ile parlar; çünkü her ikisi de aynı felsefeyi paylaşır: değişmez durum ve bildirimsel arayüz. ViewModel'i bir Compose ekranına bağlama şekli şöyledir:
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
viewModel: UserProfileMviViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// Collect the single state
val state by viewModel.state.collectAsStateWithLifecycle()
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is UserProfileSideEffect.ShowSuccess -> {
// Show snackbar
}
is UserProfileSideEffect.NavigateTo -> {
// Navigate
}
}
}
}
// Purely declarative UI based on state
UserProfileContent(
state = state,
onIntent = viewModel::onIntent
)
}
@Composable
private fun UserProfileContent(
state: UserProfileState,
onIntent: (UserProfileIntent) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Conditional rendering based on the single state
when {
state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
state.error != null -> {
ErrorMessage(
message = state.error,
onRetry = {
state.user?.id?.let {
onIntent(UserProfileIntent.LoadProfile(it))
}
}
)
}
state.user != null -> {
ProfileCard(
user = state.user,
isEditing = state.isEditing,
onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
onSaveClick = { name, bio ->
onIntent(UserProfileIntent.SaveProfile(name, bio))
},
onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
)
}
}
}
}Arayüz, durumun saf bir fonksiyonu haline gelir: öngörülebilir, test edilebilir ve gizli yan etkilerden arındırılmış.
Ayrıntılı Karşılaştırma
Her iki deseni de pratikte gördükten sonra, üretimde gerçekten önemli olan kriterlere göre karşılaştırma yapılabilir.
Durum Yönetimi
Temel fark durum yönetiminde yatmaktadır. Bu ayrım uzun vadeli sürdürülebilirliği doğrudan etkiler.
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
// Multiple sources of truth - manual synchronization needed
private val _users = MutableStateFlow<List<User>>(emptyList())
private val _selectedUser = MutableStateFlow<User?>(null)
private val _isLoading = MutableStateFlow(false)
private val _searchQuery = MutableStateFlow("")
// What happens if _selectedUser points to a user
// that's no longer in _users after a refresh?
// → Inconsistent state that's hard to detect
}
// MVI: consistent state by construction
class MviViewModel : ViewModel() {
// Single source of truth - inconsistencies are impossible
private val _state = MutableStateFlow(UsersState())
data class UsersState(
val users: List<User> = emptyList(),
val selectedUser: User? = null, // Always consistent with users
val isLoading: Boolean = false,
val searchQuery: String = ""
)
// Each update automatically maintains invariants
private fun selectUser(userId: String) {
_state.update { currentState ->
currentState.copy(
selectedUser = currentState.users.find { it.id == userId }
)
}
}
}MVVM'de tutarsız durumlar genellikle yeniden üretilmesi zor aralıklı hatalar olarak kendini gösterir. MVI'de geçersiz bir durum deterministik biçimde geçersizdir.
Mimarinin Test Edilebilirliği
Her iki mimari de test edilebilir olsa da MVI, öngörülebilirliği sayesinde önemli bir avantaj sunar.
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
val viewModel = MvvmViewModel(fakeRepository)
// Observe multiple flows simultaneously
val users = mutableListOf<List<User>>()
val loadingStates = mutableListOf<Boolean>()
val job1 = launch { viewModel.users.toList(users) }
val job2 = launch { viewModel.isLoading.toList(loadingStates) }
viewModel.loadUsers()
advanceUntilIdle()
// Assertions on different flows
assertThat(users.last()).isEqualTo(expectedUsers)
assertThat(loadingStates).containsExactly(false, true, false)
job1.cancel()
job2.cancel()
}
// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
val viewModel = MviViewModel(fakeRepository)
// Collect all states in order
val states = mutableListOf<UsersState>()
val job = launch { viewModel.state.toList(states) }
// Send the intent
viewModel.onIntent(UsersIntent.LoadUsers)
advanceUntilIdle()
// Verify the exact state sequence
assertThat(states).containsExactly(
UsersState(), // Initial
UsersState(isLoading = true), // Loading
UsersState(users = expectedUsers, isLoading = false) // Success
)
job.cancel()
}MVI, durum geçişlerinin tam sırasını test etmeyi mümkün kılar; bu özellikle çok etkileşimli karmaşık ekranlar için değerlidir.
Karmaşıklık ve Tekrar Eden Kod
Kompromisler konusunda dürüst olmak gerekir. MVI daha fazla tekrar eden kod ve kavramların daha derin anlaşılmasını gerektirir.
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name.asStateFlow()
fun updateName(newName: String) {
_name.value = newName
}
}
// Total: ~10 lines
// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
private val _state = MutableStateFlow(SimpleState())
val state: StateFlow<SimpleState> = _state.asStateFlow()
fun onIntent(intent: SimpleIntent) {
when (intent) {
is SimpleIntent.UpdateName -> {
_state.update { it.copy(name = intent.name) }
}
}
}
}
data class SimpleState(val name: String = "")
sealed class SimpleIntent {
data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~20 linesBasit bir ekran için MVI aşırı gelebilir. Ancak bu yapı, ekran karmaşıklık kazandıkça meyvesini verir.
Android mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
MVVM Ne Zaman Tercih Edilmeli?
MVVM çeşitli durumlarda pragmatik seçim olmayı sürdürür:
Mevcut Projeler
Uygulama zaten MVVM kullanıyorsa MVI'ye geçiş önemli çaba gerektirir. Mevcut MVVM yapısını iyileştirmek genellikle daha akıllıca bir karardır.
Junior veya Karma Ekipler
MVVM daha erişilebilirdir. Yeni başlayan geliştiricilerden oluşan bir ekip MVVM ile MVI'ye kıyasla daha hızlı üretken olur.
Basit Ekranlar
Az sayıda durum ve etkileşime sahip ekranlar için MVI orantısız bir karmaşıklık ekler.
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
val darkMode = preferencesRepository.darkModeFlow
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val notificationsEnabled = preferencesRepository.notificationsFlow
.stateIn(viewModelScope, SharingStarted.Lazily, true)
fun toggleDarkMode() {
viewModelScope.launch {
preferencesRepository.setDarkMode(!darkMode.value)
}
}
fun toggleNotifications() {
viewModelScope.launch {
preferencesRepository.setNotifications(!notificationsEnabled.value)
}
}
}MVI Ne Zaman Tercih Edilmeli?
MVI belirli bağlamlarda değerini kanıtlar:
Karmaşık Durumlu Uygulamalar
Bir ekranın birbiriyle bağımlı çok sayıda durumu olduğunda MVI tutarlılığı garanti eder.
// Checkout screen with complex state: MVI excels
data class CheckoutState(
val cartItems: List<CartItem> = emptyList(),
val selectedAddress: Address? = null,
val selectedPayment: PaymentMethod? = null,
val promoCode: PromoCode? = null,
val deliveryOptions: List<DeliveryOption> = emptyList(),
val selectedDelivery: DeliveryOption? = null,
val subtotal: Money = Money.ZERO,
val discount: Money = Money.ZERO,
val deliveryFee: Money = Money.ZERO,
val total: Money = Money.ZERO,
val isLoading: Boolean = false,
val error: CheckoutError? = null,
val step: CheckoutStep = CheckoutStep.CART
) {
// Verifiable invariants
init {
require(total == subtotal - discount + deliveryFee) {
"Total inconsistent with components"
}
}
}
sealed class CheckoutIntent {
data class AddItem(val item: CartItem) : CheckoutIntent()
data class RemoveItem(val itemId: String) : CheckoutIntent()
data class SelectAddress(val address: Address) : CheckoutIntent()
data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
data class ApplyPromo(val code: String) : CheckoutIntent()
object RemovePromo : CheckoutIntent()
data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
object ProceedToPayment : CheckoutIntent()
object ConfirmOrder : CheckoutIntent()
}Gerçek Zamanlı Uygulamalar
WebSocket'ler, anlık bildirimler veya gerçek zamanlı senkronizasyon içeren uygulamalar için MVI birden fazla veri akışını zarif biçimde yönetir.
Sıkı Hata Ayıklama Gereksinimleri
Düzenlenmiş alanlarda (fintech, sağlık), bir olay dizisini tam olarak yeniden üretebilme yeteneği paha biçilmezdir.
MVI, "zaman yolculuğu hata ayıklamasını" uygulamayı kolaylaştırır: tüm durumları kaydetme ve kullanıcı oturumunu yeniden oynatma.
Hibrit Yaklaşım: İki Dünyanın En İyisi
Pratikte pek çok ekip hibrit bir yaklaşım benimser: karmaşık ekranlar için MVI, basit ekranlar için sadeleştirilmiş MVVM. Önerilen desen şöyledir:
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<S> = _state.asStateFlow()
protected val currentState: S get() = _state.value
// Single entry point for intents
abstract fun onIntent(intent: I)
// Helper to update state
protected fun updateState(reducer: S.() -> S) {
_state.update { it.reducer() }
}
}
// Concrete implementation stays simple
class ProfileViewModel(
private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {
override fun onIntent(intent: ProfileIntent) {
when (intent) {
is ProfileIntent.Load -> load(intent.userId)
is ProfileIntent.Refresh -> refresh()
is ProfileIntent.ToggleFavorite -> toggleFavorite()
}
}
private fun load(userId: String) {
viewModelScope.launch {
updateState { copy(isLoading = true) }
val user = userRepository.getUser(userId)
updateState {
copy(user = user, isLoading = false)
}
}
}
private fun refresh() = load(currentState.user?.id ?: return)
private fun toggleFavorite() {
updateState {
copy(user = user?.copy(isFavorite = !user.isFavorite))
}
}
}Bu yaklaşım, aşırı tekrar eden kod olmadan MVI'nin avantajlarını sunar (tek durum, tiplenmiş intent'ler).
2026 için Öneriler
Bağlama göre iki mimari arasında seçim için öneriler şunlardır:
Compose ile Yeni Projeler için
MVI'yi baştan benimseyin. Compose ve MVI aynı felsefeyi paylaşır; başlangıç yatırımı hızla geri döner.
View Tabanlı Mevcut Projeler için
MVVM'de kalmaya devam edilmeli, ancak MVI en iyi pratikler kademeli olarak benimsenmelidir: ViewModel'de tek durum, sealed class'larla tiplenmiş eylemler.
Büyük Ekipler için
Tek bir yaklaşımda standartlaşın ve belgelendirin. Kod tabanında tutarlılık, desen seçiminden daha önemlidir.
En iyi desen, ekibin anladığı ve doğru uyguladığı desendir. İyi uygulanmış bir MVVM, kötü anlaşılmış bir MVI'yi geride bırakır.
Sonuç
MVVM ve MVI, Android uygulamalarını mimarlandırmak için her ikisi de geçerli yaklaşımlardır. MVVM basitlik ve tanışıklık sunarken MVI öngörülebilirlik ve daha kolay hata ayıklama getirir.
Karar Kontrol Listesi
- Junior ekip, basit proje, maliyetli geçiş varsa MVVM seçin
- Compose tabanlı, karmaşık durum, kritik hata ayıklama gerekliyse MVI seçin
- Hibrit yaklaşım önerilir: tek durumlu hafif MVI, aşırı mühendislik olmadan
- En yüksek öncelik: kod tabanı genelinde tutarlılık
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Seçim ne olursa olsun, bilinçli bir karar vermek için her yaklaşımın güçlü ve zayıf yönlerini anlamak şarttır. En iyi kod, ekibin uzun vadede huzurla sürdürebildiği koddur.
Etiketler
Paylaş
İlgili makaleler

Kotlin Coroutines Rehberi: Android için 2026 Kılavuzu
Android geliştirmede Kotlin coroutines konusunda uzmanlaşın: suspend fonksiyonlar, scope yapıları, dispatcher'lar ve ileri düzey kalıplar.

React Native: 2026'da Eksiksiz Bir Mobil Uygulama Gelistirmek
React Native ile iOS ve Android icin mobil uygulama gelistirmenin kapsamli rehberi. Kurulumdan yayinlamaya kadar tum temel bilgiler.