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

codeTap to expand ⛶
1// MVVM — Google's recommended approach
2@HiltViewModel
3class TaskViewModel @Inject constructor(
4 private val repository: TaskRepository
5) : ViewModel() {
6
7 private val _uiState = MutableStateFlow<TaskUiState>(TaskUiState.Loading)
8 val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
9
10 init { loadTasks() }
11
12 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 }
19
20 fun toggleComplete(taskId: String) {
21 viewModelScope.launch { repository.toggleComplete(taskId) }
22 }
23}
24
25sealed interface TaskUiState {
26 data object Loading : TaskUiState
27 data class Success(val tasks: List<Task>) : TaskUiState
28 data class Error(val message: String) : TaskUiState
29}
30
31// MVI — Strict unidirectional flow with intent/reducer
32@HiltViewModel
33class TaskMviViewModel @Inject constructor(
34 private val repository: TaskRepository
35) : ViewModel() {
36
37 private val _state = MutableStateFlow(TaskState())
38 val state: StateFlow<TaskState> = _state.asStateFlow()
39
40 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 }
47
48 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}
60
61data class TaskState(
62 val isLoading: Boolean = false,
63 val tasks: List<Task> = emptyList(),
64 val error: String? = null
65)
66
67sealed interface TaskIntent {
68 data object LoadTasks : TaskIntent
69 data class ToggleComplete(val taskId: String) : TaskIntent
70 data class DeleteTask(val taskId: String) : TaskIntent
71}
72
73// Clean Architecture — Domain Layer (pure Kotlin, no Android deps)
74class GetFilteredTasksUseCase @Inject constructor(
75 private val repository: TaskRepository // Interface, not impl
76) {
77 operator fun invoke(filter: TaskFilter): Flow<List<Task>> {
78 return repository.getTasks().map { tasks ->
79 when (filter) {
80 TaskFilter.ALL -> tasks
81 TaskFilter.ACTIVE -> tasks.filter { !it.isComplete }
82 TaskFilter.COMPLETED -> tasks.filter { it.isComplete }
83 }
84 }
85 }
86}

🏋️ Practice Exercise

Practice:

  1. Implement the same feature (todo list) in MVVM, MVI, and Clean Architecture — compare the code
  2. When would MVI be better than MVVM? Give a concrete example.
  3. Explain the Dependency Rule in Clean Architecture — why can't Data import from Presentation?
  4. How does the Repository pattern fit into Clean Architecture?
  5. 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.