MVVM vs MVI vs Clean Architecture
📖 Concept
Architecture patterns define how you structure your code for maintainability, testability, and scalability. At the senior level, you must understand trade-offs and choose patterns based on project needs.
MVVM (Model-View-ViewModel): Google's recommended pattern. The ViewModel exposes state via StateFlow/LiveData. The View observes and renders. Data flows unidirectionally.
View (Compose/XML) ← observes ← ViewModel ← Repository ← DataSource
View → events → ViewModel → Repository → DataSource
MVI (Model-View-Intent): Stricter unidirectional flow. All state changes go through a single state machine. State is a single immutable object.
View → Intent → Reducer → State → View
↑ │
└── Side Effects ──┘
Clean Architecture (Uncle Bob): Separates concerns into layers with strict dependency rules. Inner layers know nothing about outer layers.
┌─────────────────────────────────────┐
│ Presentation (UI, ViewModel) │ ← depends on →
├─────────────────────────────────────┤
│ Domain (Use Cases, Models) │ ← NO dependencies (pure Kotlin) →
├─────────────────────────────────────┤
│ Data (Repository impl, API, DB) │ ← depends on Domain →
└─────────────────────────────────────┘
When to use what:
- Small apps: MVVM is sufficient
- Complex state management: MVI (e.g., multi-step forms, real-time updates)
- Large team / long-lived app: Clean Architecture for clear boundaries
- Google's recommendation: MVVM with UDF (Unidirectional Data Flow)
💻 Code Example
1// MVVM — Google's recommended approach2@HiltViewModel3class TaskViewModel @Inject constructor(4 private val repository: TaskRepository5) : ViewModel() {67 private val _uiState = MutableStateFlow<TaskUiState>(TaskUiState.Loading)8 val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()910 init { loadTasks() }1112 private fun loadTasks() {13 viewModelScope.launch {14 repository.getTasks()15 .catch { _uiState.value = TaskUiState.Error(it.message ?: "") }16 .collect { _uiState.value = TaskUiState.Success(it) }17 }18 }1920 fun toggleComplete(taskId: String) {21 viewModelScope.launch { repository.toggleComplete(taskId) }22 }23}2425sealed interface TaskUiState {26 data object Loading : TaskUiState27 data class Success(val tasks: List<Task>) : TaskUiState28 data class Error(val message: String) : TaskUiState29}3031// MVI — Strict unidirectional flow with intent/reducer32@HiltViewModel33class TaskMviViewModel @Inject constructor(34 private val repository: TaskRepository35) : ViewModel() {3637 private val _state = MutableStateFlow(TaskState())38 val state: StateFlow<TaskState> = _state.asStateFlow()3940 fun onIntent(intent: TaskIntent) {41 when (intent) {42 is TaskIntent.LoadTasks -> loadTasks()43 is TaskIntent.ToggleComplete -> toggleTask(intent.taskId)44 is TaskIntent.DeleteTask -> deleteTask(intent.taskId)45 }46 }4748 private fun loadTasks() {49 _state.update { it.copy(isLoading = true) }50 viewModelScope.launch {51 try {52 val tasks = repository.getTasksOneShot()53 _state.update { it.copy(isLoading = false, tasks = tasks) }54 } catch (e: Exception) {55 _state.update { it.copy(isLoading = false, error = e.message) }56 }57 }58 }59}6061data class TaskState(62 val isLoading: Boolean = false,63 val tasks: List<Task> = emptyList(),64 val error: String? = null65)6667sealed interface TaskIntent {68 data object LoadTasks : TaskIntent69 data class ToggleComplete(val taskId: String) : TaskIntent70 data class DeleteTask(val taskId: String) : TaskIntent71}7273// Clean Architecture — Domain Layer (pure Kotlin, no Android deps)74class GetFilteredTasksUseCase @Inject constructor(75 private val repository: TaskRepository // Interface, not impl76) {77 operator fun invoke(filter: TaskFilter): Flow<List<Task>> {78 return repository.getTasks().map { tasks ->79 when (filter) {80 TaskFilter.ALL -> tasks81 TaskFilter.ACTIVE -> tasks.filter { !it.isComplete }82 TaskFilter.COMPLETED -> tasks.filter { it.isComplete }83 }84 }85 }86}
🏋️ Practice Exercise
Practice:
- Implement the same feature (todo list) in MVVM, MVI, and Clean Architecture — compare the code
- When would MVI be better than MVVM? Give a concrete example.
- Explain the Dependency Rule in Clean Architecture — why can't Data import from Presentation?
- How does the Repository pattern fit into Clean Architecture?
- Design the architecture for a multi-feature app with 5 modules
⚠️ Common Mistakes
Over-engineering small apps with Clean Architecture — adds unnecessary boilerplate for simple CRUD apps
Putting Android framework classes in the Domain layer — Domain should be pure Kotlin with no Android dependencies
Creating single-function Use Cases that just delegate to Repository — Use Cases should contain business logic
Not using sealed classes for UI state — leads to impossible states (loading=true AND error!=null)
Mixing concerns in ViewModel — it should only map domain data to UI state, not contain business logic
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for MVVM vs MVI vs Clean Architecture. Login to unlock this feature.