Topic 27 of 28 · Android Native Developer

Topic 13 : Application Architecture using MVC,MVP,MVVM each topic covered on one day

Lesson TL;DRTopic 13: Application Architecture using MVC, MVP, MVVM 📖 13 min read · 🎯 advanced · 🧭 Prerequisites: collectionsinjavaset, customuicomponents Why this matters Up until now, you've probably been wr...
13 min read·advanced·android · architecture · mvc · mvp

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 ViewModel and LiveData to 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; LiveData can be observed in tests
  • Lifecycle awarenessViewModel survives 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]
FeatureMVCMVPMVVM
Separation of concernsMediumHighHigh
TestabilityMediumHighHigh
Handles config changesNo (manual)No (manual)Yes (ViewModel)
BoilerplateLowMediumMedium
Android Jetpack alignmentLowMediumHigh
Best forQuick prototypesLegacy projects needing testsModern 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 Presenter and a View interface so business logic lives in a plain Java class fully decoupled from Android's Activity lifecycle.
  • MVVM uses ViewModel + LiveData from 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

Like this topic? It’s one of 28 in Android Native Developer.

Block your seat for ₹2,500 and join the next cohort.