MVC, MVP, MVVM, Clean, and MVI Explained

Table of contents
- Introduction: Why Architecture Matters
- The Evolution of Android Architecture:
- 1. MVC (Model-View-Controller) – The Classic Pattern
- 2. MVP (Model-View-Presenter) – The First Big Step
- 3. MVVM (Model-View-ViewModel) – The Modern Standard
- 4. Clean Architecture – For Large Apps
- 5. MVI (Model-View-Intent) – The Reactive Future
- Comparison Table:
- Conclusion: Which Architecture Should You Choose?

Introduction: Why Architecture Matters
If you’ve been building Android apps for a while, you’ve probably realized that as your app grows, things start to get messy—activities stuffed with business logic, network calls sprinkled everywhere, and UI updates tangled with data handling.
The solution? A solid architecture.
A good architecture:
Keeps your code clean, maintainable, and testable
Makes scaling easy as your app grows
Improves team collaboration
In this blog, we’ll take a journey through the evolution of Android architecture, from classic MVC to modern MVI, including MVP, MVVM, and Clean Architecture—with diagrams and Kotlin examples.
The Evolution of Android Architecture:
Why so many patterns? Because apps evolved.
Early Android (MVC): Activities controlled everything.
MVP: Introduced a Presenter to reduce Activity bloat.
MVVM: Google’s preferred pattern for lifecycle awareness and reactive UIs.
Clean Architecture: Perfect for enterprise-scale apps.
MVI: Great for state-driven UI like Jetpack Compose.
1. MVC (Model-View-Controller) – The Classic Pattern
What is MVC?
MVC is one of the oldest design patterns that divides an application into three interconnected components:
Model: Represents data and business logic. Handles data storage, API calls, and database operations.
View: Represents the user interface. Displays data to the user and sends user actions to the Controller.
Controller: Acts as a mediator between View and Model. It processes user input, updates the Model, and refreshes the View.
Diagram:
MVC Folder Structure:
app/
├── model/
│ └── User.kt
├── view/
│ └── activity_main.xml
├── controller/
│ └── MainActivity.kt // Acts as both Controller & View in Android
Data Flow:
Controller fetches Model data
Updates View when data changes
Kotlin Code Example:
// Model: Represents data
data class User(val name: String, val age: Int)
// View Interface: Defines UI contract
interface UserView {
fun showUser(user: User)
}
// Controller: Handles logic
class UserController(private val view: UserView) {
fun getUser() {
val user = User("John", 25)
view.showUser(user)
}
}
// View Activity: Implements View
class MainActivity : AppCompatActivity(), UserView {
private lateinit var controller: UserController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
controller = UserController(this)
controller.getUser()
}
override fun showUser(user: User) {
findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}"
}
}
Pros:
Simple and easy to start
Good for very small apps
Cons:
Activity often becomes God class (does everything)
Hard to maintain and test for big applications.
Tight coupling between UI and logic
2. MVP (Model-View-Presenter) – The First Big Step
What is MVP?
MVP is an improvement over MVC, introduced to separate UI logic from business logic more effectively.
Model: Data layer (APIs, DB).
View: UI layer, represented by Activity or Fragment (implements a
View Interface
).Presenter: Contains presentation logic, communicates with Model and updates View.
Diagram:
MVP Folder Structure
app/
├── model/
│ └── User.kt
├── presenter/
│ └── UserPresenter.kt
├── view/
│ ├── UserView.kt // Interface
│ └── MainActivity.kt // Implements UserView
Kotlin Code Example:
// Model: Represents data
data class User(val name: String, val age: Int)
// View : Interface
interface UserView {
fun showUser(user: User)
}
// Presenter
class UserPresenter(private val view: UserView) {
fun loadUser() {
val user = User("John Doe", 30)
view.showUser(user)
}
}
// View: Activity
class MainActivity : AppCompatActivity(), UserView {
private lateinit var presenter: UserPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter = UserPresenter(this)
presenter.loadUser()
}
override fun showUser(user: User) {
findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}"
}
}
Pros:
Easier to test Presenter since it’s independent of Android components.
View is passive and only displays data provided by Presenter.
Cons:
Presenter can still grow very large in complex apps.
Requires manual binding between View and Presenter.
3. MVVM (Model-View-ViewModel) – The Modern Standard
What is MVVM?
MVVM is a pattern recommended by Google for Android. It uses data binding or reactive streams to reduce boilerplate code.
Model: Responsible for data and business logic.
View: UI layer (Activity, Fragment) observes ViewModel.
ViewModel: Stores UI-related data, survives configuration changes, and exposes data via LiveData or StateFlow.
Diagram:
MVVM Folder Structure:
app/
├── model/
│ └── User.kt
├── view/
│ ├── activity_main.xml
│ └── MainActivity.kt
├── viewmodel/
│ └── UserViewModel.kt
Kotlin Code Example:
// ======================
// Model
// ======================
data class User(val name: String, val age: Int)
// ======================
// ViewModel
// Holds the UI data and business logic
// ======================
class UserViewModel : ViewModel() {
// LiveData to observe changes in UI
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
// Simulate fetching user data (normally from Repository)
fun fetchUser() {
_user.value = User("John Doe", 30)
}
}
// ======================
// View (Activity)
// Observes the ViewModel and updates the UI
// ======================
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize ViewModel
viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
// Observe LiveData for changes
viewModel.user.observe(this) { user ->
// Update UI when data changes
findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}"
}
// Fetch user data when Activity starts
viewModel.fetchUser()
}
}
Pros:
No direct reference between View and ViewModel (reduces memory leaks).
Great for reactive UI with LiveData or Flow.
Highly testable.
Cons:
- Slightly more complex than MVP for beginners.
4. Clean Architecture – For Large Apps
What is Clean Architecture?
Clean Architecture is a layered architecture proposed by Robert C. Martin (Uncle Bob). It organizes code into layers with strict dependency rules:
Presentation Layer: UI and ViewModel.
Domain Layer: Business rules, use cases, entities (pure Kotlin/Java, independent of Android).
Data Layer: Repositories, APIs, databases.
Principles:
Dependency direction is always inward (UI → Domain → Data).
Each layer is independent and testable.
Best Practice:
Use Hilt for DI
Coroutines + Flow for async tasks
Multi-module for faster builds
Diagram:
Clean Architecture Folder Structure:
app/
├── presentation/
│ ├── view/
│ │ └── MainActivity.kt
│ └── viewmodel/
│ └── UserViewModel.kt
├── domain/
│ ├── model/
│ │ └── User.kt
│ ├── repository/
│ │ └── UserRepository.kt
│ └── usecase/
│ └── GetUserUseCase.kt
├── data/
│ ├── repository/
│ │ └── UserRepositoryImpl.kt
│ ├── remote/
│ │ └── ApiService.kt
│ └── local/
│ └── UserDao.kt
Kotlin Code Example:
// =============================
// Domain Layer (Independent of frameworks)
// =============================
// Entity (Business object)
data class User(val name: String, val age: Int)
// Repository Interface (Abstraction)
interface UserRepository {
fun getUser(): User
}
// Use Case (Business logic)
class GetUserUseCase(private val repository: UserRepository) {
operator fun invoke(): User {
// Here you can add additional business rules if needed
return repository.getUser()
}
}
// =============================
// Data Layer (Implements Repository)
// =============================
// Repository Implementation (Uses data sources)
class UserRepositoryImpl : UserRepository {
override fun getUser(): User {
// In real-world, fetch from API or Database
return User("John Doe", 30)
}
}
// =============================
// Presentation Layer (UI + ViewModel)
// =============================
class UserViewModel(private val getUserUseCase: GetUserUseCase) : ViewModel() {
// LiveData to expose data to UI
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun fetchUser() {
// Call the UseCase to get user data
_user.value = getUserUseCase()
}
}
// =============================
// View (Activity)
// =============================
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Manual DI for simplicity (Hilt/Dagger recommended in real projects)
val repository = UserRepositoryImpl()
val useCase = GetUserUseCase(repository)
viewModel = UserViewModel(useCase)
// Observe LiveData to update UI
viewModel.user.observe(this) { user ->
findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}"
}
// Fetch user when screen loads
viewModel.fetchUser()
}
}
Pros:
Scalable and maintainable for large applications.
Allows easy replacement of frameworks or data sources.
Cons:
- Initial setup requires more effort and boilerplate.
5. MVI (Model-View-Intent) – The Reactive Future
What is MVI?
MVI is a reactive, unidirectional architecture inspired by Redux. It focuses on managing state in a predictable way.
Model: Represents the entire UI state as an immutable object.
View: Displays the state and sends user intents.
Intent: Represents user actions, which trigger state changes.
Diagram:
MVI Folder Structure:
app/
├── intent/
│ └── UserIntent.kt
├── model/
│ └── UserState.kt
├── view/
│ └── MainActivity.kt
├── viewmodel/
│ └── UserViewModel.kt
├── repository/
│ └── UserRepository.kt
Kotlin Code Example:
// Intent: Represents user actions
sealed class UserIntent {
object LoadUser : UserIntent()
}
// State: Represents UI state
data class UserState(val name: String = "", val age: Int = 0)
// ViewModel: Processes intents and updates state
class UserViewModel : ViewModel() {
private val _state = MutableLiveData<UserState>()
val state: LiveData<UserState> = _state
// Handle user intents
fun processIntent(intent: UserIntent) {
when (intent) {
is UserIntent.LoadUser -> {
// Update state when user is loaded
_state.value = UserState("John Doe", 30)
}
}
}
}
// View (Activity/Fragment): Observes state and sends intents
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
// Observe state changes
viewModel.state.observe(this) { state ->
findViewById<TextView>(R.id.textView).text = "${state.name}, ${state.age}"
}
// Send an intent to load user
findViewById<Button>(R.id.button).setOnClickListener {
viewModel.processIntent(UserIntent.LoadUser)
}
}
}
Pros:
Single source of truth for UI state.
Easy debugging (predictable state changes).
Ideal for Jetpack Compose or declarative UI.
Cons:
- Can be verbose and complex for simple apps.
Comparison Table:
Pattern | Complexity | Testability | Data Flow | Best For |
MVC | Low | Low | Bi-directional (Controller updates View & Model) | Small apps |
MVP | Medium | Medium | Bi-directional (Presenter mediates View & Model) | Medium apps |
MVVM | Medium | High | One-way reactive (View observes ViewModel) | Most apps |
Clean | High | Very High | Layered one-way (UI → Domain → Data) | Large teams |
MVI | High | High | Unidirectional (Intent → State → View) | Compose UIs |
Conclusion: Which Architecture Should You Choose?
Android architecture patterns have evolved for a reason—to make your code maintainable, scalable, and testable. There’s no one-size-fits-all solution; the right pattern depends on your app’s complexity, team size, and long-term goals.
MVC – Use only for tiny apps or quick prototypes.
MVP – Works well for small to medium apps where you want some structure but don’t need advanced features.
MVVM – The modern standard for most Android apps, especially when using Jetpack components or Compose.
Clean Architecture – Best for enterprise apps, large teams, and projects requiring scalability and testability.
MVI – Ideal for reactive UIs and Jetpack Compose, providing a single source of truth for state.
My Recommendation:
Start with MVVM if you’re building modern apps.
Move to Clean Architecture for large, complex projects.
Consider MVI if you’re developing with Jetpack Compose or need unidirectional data flow.
Your Turn: Which architecture pattern do you use in your apps? Drop a comment below and let’s discuss!
Subscribe to my newsletter
Read articles from Amitabh Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Amitabh Sharma
Amitabh Sharma
I am a mobile application developer with more than 10 years of experience in developing native and cross platform mobile apps.