Topic 13: Application Architecture using MVC, MVP, MVVM
📖 13 min read · 🎯 advanced · 🧭 Prerequisites: collections-in-java-set, custom-ui-components
Why this matters
Up until now, you've probably been writing Android code and just… putting things wherever made sense in the moment. A button click here, some data logic there, maybe some UI updates mixed right in. It works — until it doesn't. Once the app grows, or a teammate joins, or you need to fix a bug at 2am, that tangled code becomes a nightmare. That's exactly why patterns like MVC, MVP, and MVVM exist. We're going to spend three days on this — one pattern per day — because each one is a different way of teaching your Android code to stay organized and stop stepping on its own feet.
What You'll Learn
- How MVC divides an Android app into Model, View, and Controller and where each class lives
- How MVP refines MVC by introducing a Presenter interface so the View becomes fully passive
- How MVVM leverages Android's
ViewModelandLiveDatato enable observable, lifecycle-aware UI updates - The trade-offs between all three patterns so you can choose the right one for a project
The Analogy
Think of a busy restaurant. The kitchen (Model) owns all the recipes and ingredients — it never speaks directly to a customer. The dining room (View) is what customers see: tables, menus, plates arriving. The waiter (Controller/Presenter/ViewModel) is the go-between: taking orders from the dining room, passing them to the kitchen, then carrying results back. MVC makes the waiter a direct relay. MVP gives the waiter a strict script so the dining room never has to think. MVVM puts a live order board on the wall so the dining room updates itself the moment the kitchen finishes — the waiter just posts the update and steps back.
Chapter 1: Day 1 — MVC (Model-View-Controller)
Overview
MVC is one of the oldest and most widely used architectural patterns. It divides an application into three interconnected components:
- Model — manages data and business logic
- View — displays data to the user and sends user actions to the Controller
- Controller — acts as intermediary between Model and View, processing input and updating the Model
Benefits
- Separation of Concerns — clearly separates UI from business logic
- Reusability — components can be reused independently
- Testability — facilitates unit testing of individual components
MVC in Action: A Simple Android App
1. Model — User.java
// User.java
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
2. View — activity_main.xml
<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Name" />
<EditText
android:id="@+id/email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email" />
<Button
android:id="@+id/submit_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Submit" />
<TextView
android:id="@+id/display_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp" />
</LinearLayout>
3. Controller — UserController.java and MainActivity.java
// UserController.java
package com.example.mvcapp;
public class UserController {
private User user;
public void addUser(String name, String email) {
user = new User(name, email);
}
public User getUser() {
return user;
}
}
// MainActivity.java
package com.example.mvcapp;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private EditText nameInput;
private EditText emailInput;
private TextView displayText;
private UserController userController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nameInput = findViewById(R.id.name_input);
emailInput = findViewById(R.id.email_input);
displayText = findViewById(R.id.display_text);
Button submitButton = findViewById(R.id.submit_button);
userController = new UserController();
submitButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = nameInput.getText().toString();
String email = emailInput.getText().toString();
userController.addUser(name, email);
displayUserDetails();
}
});
}
private void displayUserDetails() {
User user = userController.getUser();
if (user != null) {
displayText.setText("Name: " + user.getName() + "\nEmail: " + user.getEmail());
}
}
}
Notice that MainActivity here plays both View and Controller roles — a classic Android MVC quirk that MVP was designed to fix.
Chapter 2: Day 2 — MVP (Model-View-Presenter)
Overview
MVP is a derivative of MVC that addresses its tight coupling by introducing a Presenter. The Presenter owns business logic and updates the View through a well-defined interface, making the View entirely passive.
- Model — manages data and business logic
- View — displays data and passes user interactions to the Presenter (implemented as an interface)
- Presenter — interacts with the Model to fetch/update data and calls View methods directly
Benefits
- Better Separation — clearer division of responsibilities than MVC
- Testability — the Presenter can be unit-tested with a mock View, no Android framework needed
- Flexible Views — swap out different view implementations without touching the Presenter
MVP in Action
1. Model — User.java
Same User class as before:
// User.java
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}
2. View Interface — UserView.java
// UserView.java
public interface UserView {
void showUser(User user);
void showError(String message);
}
The interface is the contract. The Activity implements it; the Presenter only knows about the interface — never about Android's Activity class.
3. Presenter — UserPresenter.java
// UserPresenter.java
public class UserPresenter {
private UserView userView;
public UserPresenter(UserView userView) {
this.userView = userView;
}
public void addUser(String name, String email) {
if (name.isEmpty() || email.isEmpty()) {
userView.showError("Name and email cannot be empty");
} else {
User user = new User(name, email);
userView.showUser(user);
}
}
}
Validation now lives in the Presenter, not the Activity. You can test this method with any UserView mock.
4. View Implementation — MainActivity.java
// MainActivity.java
package com.example.mvpapp;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements UserView {
private EditText nameInput;
private EditText emailInput;
private TextView displayText;
private UserPresenter userPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nameInput = findViewById(R.id.name_input);
emailInput = findViewById(R.id.email_input);
displayText = findViewById(R.id.display_text);
Button submitButton = findViewById(R.id.submit_button);
userPresenter = new UserPresenter(this);
submitButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = nameInput.getText().toString();
String email = emailInput.getText().toString();
userPresenter.addUser(name, email);
}
});
}
@Override
public void showUser(User user) {
displayText.setText("Name: " + user.getName() + "\nEmail: " + user.getEmail());
}
@Override
public void showError(String message) {
displayText.setText(message);
}
}
The Activity now only forwards events and renders results — it makes zero decisions.
Chapter 3: Day 3 — MVVM (Model-View-ViewModel)
Overview
MVVM separates the UI from business logic by using an observable ViewModel. Rather than the Presenter calling View methods directly, the ViewModel exposes LiveData streams that the View observes. Android Jetpack's androidx.lifecycle package makes this pattern first-class on Android.
- Model — manages data and business logic
- View — displays data and forwards user interactions to the ViewModel; observes
LiveData - ViewModel — acts as bridge between View and Model, exposes data through observable properties (
LiveData/MutableLiveData), survives configuration changes
Benefits
- Two-Way Data Binding — simplifies synchronization between View and underlying data
- Separation of Concerns — clean boundary between UI and business logic
- Testability — ViewModel can be tested without any UI;
LiveDatacan be observed in tests - Lifecycle awareness —
ViewModelsurvives screen rotations; no more data loss on config change
MVVM in Action
1. Model — User.java
// User.java
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}
2. ViewModel — UserViewModel.java
// UserViewModel.java
package com.example.mvvmapp;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class UserViewModel extends ViewModel {
private final MutableLiveData<User> user = new MutableLiveData<>();
private final MutableLiveData<String> error = new MutableLiveData<>();
public LiveData<User> getUser() {
return user;
}
public LiveData<String> getError() {
return error;
}
public void addUser(String name, String email) {
if (name.isEmpty() || email.isEmpty()) {
error.setValue("Name and email cannot be empty");
} else {
user.setValue(new User(name, email));
}
}
}
MutableLiveData is writable from inside the ViewModel; the public API exposes read-only LiveData so Views cannot push values.
3. View — activity_main.xml
<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<EditText
android:id="@+id/name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Name" />
<EditText
android:id="@+id/email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email" />
<Button
android:id="@+id/submit_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Submit" />
<TextView
android:id="@+id/display_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp" />
</LinearLayout>
4. Activity — MainActivity.java
// MainActivity.java
package com.example.mvvmapp;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
public class MainActivity extends AppCompatActivity {
private EditText nameInput;
private EditText emailInput;
private TextView displayText;
private UserViewModel userViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nameInput = findViewById(R.id.name_input);
emailInput = findViewById(R.id.email_input);
displayText = findViewById(R.id.display_text);
Button submitButton = findViewById(R.id.submit_button);
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
userViewModel.getUser().observe(this, new Observer<User>() {
@Override
public void onChanged(User user) {
displayText.setText("Name: " + user.getName() + "\nEmail: " + user.getEmail());
}
});
userViewModel.getError().observe(this, new Observer<String>() {
@Override
public void onChanged(String errorMessage) {
displayText.setText(errorMessage);
}
});
submitButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = nameInput.getText().toString();
String email = emailInput.getText().toString();
userViewModel.addUser(name, email);
}
});
}
}
ViewModelProvider ensures the same UserViewModel instance survives rotation — no need to re-fetch data after the screen turns.
Chapter 4: Putting It Together — Choosing the Right Pattern
flowchart TD
A[Need Android Architecture?] --> B{Team size / project complexity}
B -->|Small / prototype| C[MVC\nFast, familiar, Activity = Controller+View]
B -->|Medium / testability required| D[MVP\nPresenter interface, mock-able View]
B -->|Large / lifecycle-critical / Jetpack| E[MVVM\nLiveData, ViewModel, config-change safe]
C --> F[Risk: tight coupling in Activity]
D --> G[Risk: manual View null-check on rotation]
E --> H[Best fit for modern Android / Compose-ready]
| Feature | MVC | MVP | MVVM |
|---|---|---|---|
| Separation of concerns | Medium | High | High |
| Testability | Medium | High | High |
| Handles config changes | No (manual) | No (manual) | Yes (ViewModel) |
| Boilerplate | Low | Medium | Medium |
| Android Jetpack alignment | Low | Medium | High |
| Best for | Quick prototypes | Legacy projects needing tests | Modern Jetpack apps |
Rule of thumb: start new Android projects with MVVM + Jetpack. Use MVP when you're adding tests to an existing MVC codebase without a full rewrite.
🧪 Try It Yourself
Task: Build the MVVM user-form app from Day 3 and add a third LiveData<Boolean> field called isLoading. When addUser() is called, set isLoading to true for 1 second (use Handler.postDelayed) then set the final user value and flip isLoading back to false. In MainActivity, observe isLoading and show/hide a ProgressBar accordingly.
Success criterion: Tapping Submit makes the ProgressBar visible for ~1 second, then the name/email text appears and the ProgressBar disappears.
Starter snippet for the ViewModel:
// inside UserViewModel.java
import android.os.Handler;
import android.os.Looper;
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void addUser(String name, String email) {
if (name.isEmpty() || email.isEmpty()) {
error.setValue("Name and email cannot be empty");
return;
}
isLoading.setValue(true);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
user.setValue(new User(name, email));
isLoading.setValue(false);
}, 1000);
}
🔍 Checkpoint Quiz
Q1. In the MVC example above, MainActivity acts as both View and Controller. Why is this considered a limitation of MVC on Android?
A) Android forbids separate Controller classes
B) It makes Activities large and hard to test in isolation
C) The Model cannot compile without an Activity reference
D) XML layouts cannot be used without a separate Controller class
Q2. Given this MVP Presenter snippet, what happens when both fields are empty and the user taps Submit?
public void addUser(String name, String email) {
if (name.isEmpty() || email.isEmpty()) {
userView.showError("Name and email cannot be empty");
} else {
User user = new User(name, email);
userView.showUser(user);
}
}
A) A new User object is created with empty strings
B) The app crashes with a NullPointerException
C) showError("Name and email cannot be empty") is called on the View
D) Nothing happens — the condition is never reached
Q3. In the MVVM example, why is the field declared as MutableLiveData internally but exposed as LiveData publicly?
private final MutableLiveData<User> user = new MutableLiveData<>();
public LiveData<User> getUser() { return user; }
A) LiveData is faster at runtime than MutableLiveData
B) To prevent the Activity from calling setValue() and bypassing the ViewModel's logic
C) MutableLiveData cannot be observed by an Activity
D) Android Studio's linter requires this pattern
Q4. You are joining a team maintaining a 3-year-old Android app that uses MVC. The team wants to add unit tests for business logic without rewriting the entire codebase. Which migration step makes the most immediate impact?
A) Rewrite everything in MVVM from scratch
B) Extract business logic from Activities into Presenter classes following the MVP pattern
C) Move all logic into the XML layout using data binding
D) Replace the Model layer with a Room database
A1. B — When an Activity is both View and Controller, it becomes a large, hard-to-test "God class." You cannot instantiate it without the Android runtime, so business logic inside it cannot be tested with plain JUnit.
A2. C — The || condition is true when either field is empty. With both empty, showError fires immediately, displaying the error message in displayText.
A3. B — Exposing a read-only LiveData reference enforces that only the ViewModel can post new values. The Activity can observe but cannot push data, keeping the data-flow unidirectional and the ViewModel's invariants intact.
A4. B — Extracting business logic into Presenter classes (MVP) is the pragmatic incremental step. The existing Activity shell stays, but logic moves to a plain Java class that is trivially testable with mock UserView objects, with no need for Robolectric or instrumented tests.
🪞 Recap
- MVC splits an app into Model, View, and Controller, but on Android the Activity often blurs the View/Controller boundary, reducing testability.
- MVP introduces a
Presenterand aViewinterface so business logic lives in a plain Java class fully decoupled from Android's Activity lifecycle. - MVVM uses
ViewModel+LiveDatafrom Android Jetpack to make the View an observer, surviving configuration changes automatically. - All three patterns share the same goal — separation of concerns — but each trades boilerplate for lifecycle safety and testability in different ways.
- For new Android projects, MVVM with Jetpack is the industry default; MVP remains a practical migration target for legacy MVC codebases.
📚 Further Reading
- Android Architecture Guide (official) — Google's recommended layered architecture built on MVVM
- ViewModel overview — source of truth on
ViewModelProvider, scopes, andSavedStateHandle - LiveData overview — lifecycle-aware observable data holder explained in depth
- Guide to app architecture (Codelab) — hands-on MVVM codelab from Android training
- ⬅️ Previous: Custom UI Components
- ➡️ Next: Operators on Collections