Skip to content
Module 10 of 20

Android clean architecture and MVVM

Android clean architecture with MVVM, the repository pattern, use cases, layers, and SOLID boundaries.

Learning Objectives

By the end of this module, you’ll understand:

  • Why architecture exists
  • Why “Massive Activity” is a problem
  • Separation of Concerns (SoC)
  • SOLID principles
  • MVC, MVP, MVVM, MVI comparison
  • Why Google recommends MVVM
  • ViewModel internals
  • LiveData vs StateFlow vs SharedFlow
  • Repository Pattern
  • Use Cases (Domain Layer)
  • Clean Architecture
  • Dependency Injection (conceptual)
  • Modern Android project structure
  • Common architectural mistakes

Part 1 — Why Do We Need Architecture?

Let’s begin with a simple login screen.

Many beginners write:

class LoginActivity : AppCompatActivity() {

    override fun onCreate(...) {

        loginButton.setOnClickListener {

            val email = emailEditText.text.toString()
            val password = passwordEditText.text.toString()

            if(email.isEmpty()){
                ...
            }

            val response = api.login(email,password)

            if(response.success){

                database.saveUser(response.user)

                startActivity(...)

            }

        }
    }
}

Looks fine.

Until the app grows.


Suppose six months later you add:

  • Analytics
  • Logging
  • Offline support
  • Retry mechanism
  • Cache
  • Encryption
  • Unit tests
  • Biometrics
  • Multiple APIs

Now the Activity becomes:

LoginActivity

↓

1500 Lines

↓

2000 Lines

↓

3000 Lines

This is called a Massive Activity (or Massive Fragment).


2. Why Massive Activities Are Bad

Imagine a restaurant.

One chef:

  • Takes orders
  • Cooks
  • Cleans tables
  • Washes dishes
  • Collects payments
  • Buys groceries

Possible?

Technically.

Efficient?

No.

Instead:

Cashier

↓

Chef

↓

Waiter

↓

Cleaner

Everyone has one responsibility.

Software should work the same way.


3. Separation of Concerns (SoC)

One of the most fundamental software engineering principles.

Definition:

Every class should have one well-defined responsibility.

Example:

UI

↓

Business Logic

↓

Data

Three completely different concerns.

Mixing them leads to chaos.


Suppose user taps Login.

What actually happens?

User

↓

Activity

↓

Validate Input

↓

API Call

↓

Parse Response

↓

Save Database

↓

Navigate

↓

Analytics

↓

Logs

Should one class perform all of these?

No.

Architecture distributes responsibilities.


Part 2 — SOLID Principles

Before Android architecture, we need SOLID.

These principles influence almost every modern architecture.


S — Single Responsibility Principle (SRP)

Definition:

A class should have only one reason to change.

Bad:

LoginActivity

↓

UI

↓

API

↓

Database

↓

Analytics

Good:

LoginActivity

↓

LoginViewModel

↓

LoginRepository

↓

ApiService

Each class has one responsibility.


Example:

UserRepository

↓

Only Handles User Data

Not UI.

Not Navigation.

Not Analytics.


O — Open/Closed Principle

Definition:

Open for extension, closed for modification.

Imagine payment methods.

Bad:

if(card)

else if(paypal)

else if(upi)

else if(wallet)

Every new payment method modifies old code.

Better:

PaymentProcessor

↓

CardProcessor

↓

PaypalProcessor

↓

UpiProcessor

Add new classes.

Don’t modify existing ones.


L — Liskov Substitution Principle

Definition:

Subclasses should be replaceable with their parent without breaking behavior.

Bad example:

Bird

↓

Penguin

If Bird.fly() exists,

Penguin breaks the contract.

Poor inheritance design.

Android example:

A custom RecyclerView.Adapter should still behave like a normal adapter.


I — Interface Segregation Principle

Don’t force clients to implement methods they don’t need.

Bad:

Animal

↓

Eat()

Fly()

Swim()

Dog implements Fly()?

Makes no sense.

Create smaller interfaces.


D — Dependency Inversion Principle

One of the most important principles.

Instead of:

ViewModel

↓

Retrofit

Use:

ViewModel

↓

Repository Interface

↓

Retrofit Implementation

High-level modules depend on abstractions, not concrete implementations.

This enables testing and flexibility.


Part 3 — Evolution of Android Architectures


MVC

Original approach.

View

↓

Controller

↓

Model

Android Activity became the Controller.

Problem:

Activity also handled View responsibilities.

Eventually:

Activity

↓

3000 Lines

MVC on Android often degenerated into “Massive Controller.”


MVP

Google’s early solution.

View

↓

Presenter

↓

Model

The View became passive.

Presenter contained business logic.

Problems:

  • Lots of interfaces
  • Boilerplate
  • Harder lifecycle management

MVVM

Google’s recommended architecture.

View

↓

ViewModel

↓

Repository

↓

Data Source

Notice something.

The View doesn’t know where data comes from.

The ViewModel hides that complexity.


MVI

Modern unidirectional architecture.

Intent

↓

Reducer

↓

State

↓

UI

Popular in Compose and functional programming circles.

Excellent for predictable state.

Steeper learning curve.


Comparison

ArchitectureBiggest Problem
MVCMassive Activities
MVPBoilerplate
MVVMExcellent balance
MVIMore complexity but strong state management

Part 4 — MVVM Deep Dive


Why MVVM?

Imagine:

Button Click

↓

Load Users

↓

Display Users

Without MVVM:

Activity

↓

API

↓

Database

↓

UI

Everything is coupled.


With MVVM:

Activity

↓

ViewModel

↓

Repository

↓

Network

↓

Database

Each layer has one job.


The View

Responsibilities:

  • Display UI
  • Observe state
  • Send user actions

Nothing else.

Example:

User Click

↓

ViewModel.login()

The View doesn’t know:

  • Retrofit
  • Room
  • Firebase

The ViewModel

This is the brain of the screen.

Responsibilities:

  • Business logic
  • State management
  • Coordinate repositories
  • Survive configuration changes

It should not know about Android Views like Button or TextView.


Example flow:

User Click

↓

ViewModel

↓

Repository

↓

API

↓

Repository

↓

ViewModel

↓

UI State Updated

Why Does ViewModel Survive Rotation?

Remember Module 5.

Rotation:

Activity Destroyed

↓

Activity Created

ViewModel is stored by a ViewModelStore owned by the Activity (or Fragment).

Activity

↓

ViewModelStore

↓

ViewModel

When a new Activity instance is created after a configuration change, it retrieves the existing ViewModel from the store.

This avoids unnecessary API calls and preserves screen state.


Part 5 — State Management

The ViewModel exposes state.

Historically:

LiveData

Today, Google generally recommends:

StateFlow

For one-time events:

SharedFlow

Let’s understand why.


LiveData

Lifecycle-aware observable.

ViewModel

↓

LiveData

↓

Activity

Pros:

  • Lifecycle-aware
  • Simple
  • XML-friendly

Cons:

  • Android-specific
  • Less flexible than Flow

StateFlow

Think of it as:

A stream that always has a current value.

Example:

Loading

↓

Success

↓

Error

↓

Retry

The UI always knows the latest state.

Ideal for screen state.


SharedFlow

Some events should not replay.

Example:

Show Toast

↓

Navigate

↓

Snackbar

You don’t want a rotation to show the same toast again.

SharedFlow is designed for one-time events.


Rule of Thumb

Use CaseRecommended
Screen UI StateStateFlow
One-time eventsSharedFlow
Legacy XML appsLiveData (or StateFlow with collectors)

Part 6 — Repository Pattern

Suppose your ViewModel needs users.

Without Repository:

ViewModel

↓

Retrofit

↓

Room

↓

Firebase

Too many dependencies.

Instead:

ViewModel

↓

Repository

↓

Network

↓

Database

↓

Cache

The Repository becomes the single source of truth for data access.


Imagine the network fails.

Repository decides:

Network?

↓

Yes → Return API

↓

No

↓

Return Cache

The ViewModel doesn’t care where the data came from.


Part 7 — Clean Architecture

Robert C. Martin (Uncle Bob) proposed Clean Architecture.

Core idea:

Dependencies should point inward.

Typical layers:

Presentation

↓

Domain

↓

Data

Presentation Layer

Contains:

  • Activity
  • Fragment
  • Compose UI
  • ViewModel

Responsible for interacting with users.


Domain Layer

Pure business logic.

Contains:

Use Cases

Entities

Interfaces

Example:

LoginUseCase

↓

Authenticate User

No Android imports.

No Retrofit.

No Room.

This layer can often be reused across platforms.


Data Layer

Contains:

Repository

↓

Remote

↓

Local

↓

Cache

This layer knows how to fetch and store data.


Complete Flow

Imagine user taps Login.

UI

↓

ViewModel

↓

LoginUseCase

↓

UserRepository

↓

Retrofit

↓

API

Response:

API

↓

Repository

↓

UseCase

↓

ViewModel

↓

StateFlow

↓

UI

Notice the clear direction of dependencies.


Part 8 — Dependency Injection (Conceptual)

Suppose:

val repository = UserRepository()

Problem:

The ViewModel decides which repository to use.

Instead:

ViewModel

↓

Needs Repository

↓

DI Provides It

The ViewModel simply declares its dependency.


Benefits:

  • Easier testing
  • Replace implementations
  • Looser coupling

In Android, Hilt is Google’s recommended DI framework.

We’ll dedicate an entire module to DI later.


A common feature-first organization:

app/
│
├── data/
│   ├── remote/
│   ├── local/
│   ├── repository/
│
├── domain/
│   ├── model/
│   ├── repository/
│   └── usecase/
│
├── ui/
│   ├── login/
│   ├── home/
│   └── profile/
│
├── di/
│
└── common/

Many teams organize by feature rather than by technical layer alone, especially in larger apps.


Common Architecture Mistakes

❌ Putting business logic in Activities

Activities should coordinate UI, not perform business rules.


❌ Calling Retrofit directly from the UI

Always go through the Repository (and often a Use Case).


❌ Repositories updating UI

Repositories return data.

The ViewModel decides how that data affects the UI.


❌ ViewModel holding View references

Never keep references to:

  • Activity
  • Fragment
  • TextView
  • Button

This causes memory leaks and violates separation of concerns.


❌ Everything as a Singleton

Not every object should live for the entire app lifetime.

Choose scopes appropriate to the object’s responsibility.


Real-World Example

Let’s trace a product search in an e-commerce app.

User types "Laptop"
        │
        ▼
SearchScreen (Compose)
        │
        ▼
SearchViewModel
        │
        ▼
SearchProductsUseCase
        │
        ▼
ProductRepository
      ┌─┴─────────────┐
      ▼               ▼
Remote API        Local Cache
      │               │
      └───────┬───────┘
              ▼
      Repository Result
              ▼
SearchViewModel updates StateFlow
              ▼
Compose recomposes with search results

Each layer has a single responsibility:

  • UI displays state.
  • ViewModel manages state.
  • Use Case applies business rules.
  • Repository coordinates data.
  • Data sources fetch and store data.

Mental Model

Think of a hospital.

Patient
   │
Reception
   │
Doctor
   │
Laboratory
   │
Pharmacy

The receptionist doesn’t perform surgery.

The doctor doesn’t manufacture medicine.

Each role has a clear responsibility.

A well-architected Android app follows the same philosophy.


Best Practices

  • Keep UI “dumb” and state-driven.
  • Put business logic in the ViewModel or Use Cases, not Activities.
  • Expose immutable UI state (StateFlow) from ViewModels.
  • Use Repositories as the single entry point for data.
  • Keep the Domain layer independent of Android APIs when using Clean Architecture.
  • Depend on interfaces instead of implementations.
  • Prefer constructor injection with Hilt for dependencies.

Interview Questions

  1. Why do Android apps need an architecture?
  2. What problems do Massive Activities cause?
  3. Explain the SOLID principles with Android examples.
  4. Compare MVC, MVP, MVVM, and MVI.
  5. Why is MVVM Google’s recommended architecture?
  6. Why does a ViewModel survive configuration changes?
  7. When would you use StateFlow vs SharedFlow?
  8. What is the Repository pattern, and why is it useful?
  9. Explain the layers of Clean Architecture.
  10. What problem does Dependency Injection solve?

Module 10 Summary

You now understand the architectural principles behind modern Android development:

  • Separation of Concerns keeps responsibilities clear.
  • SOLID principles make code maintainable and extensible.
  • MVVM separates UI from business logic.
  • ViewModel manages screen state and survives configuration changes.
  • Repositories abstract data access.
  • Clean Architecture organizes applications into Presentation, Domain, and Data layers.
  • Dependency Injection reduces coupling and improves testability.

At this point, you have the conceptual foundation used in most professional Android codebases.


Next Module: Concurrency, Coroutines & Flow

This is where you’ll learn how Android performs work without freezing the UI.

We’ll cover:

  • Why Android has a Main Thread
  • What causes ANRs (Application Not Responding)
  • Threads and their limitations
  • Kotlin Coroutines in depth
  • launch, async, withContext
  • Structured Concurrency
  • Coroutine scopes (viewModelScope, lifecycleScope)
  • Exception handling and cancellation
  • Cold vs Hot Flows
  • Flow, StateFlow, and SharedFlow internals
  • Real-world patterns for networking, databases, and UI updates

This is one of the most technically rich topics in Android, and mastering it is essential for building responsive, modern applications.