Android Architecture (MVVM, Repository, Clean Architecture & SOLID)
Learn professional Android architecture patterns including MVVM, Repository, Clean Architecture, and SOLID principles.
Module 10: Android Architecture (MVVM, Repository, Clean Architecture & SOLID)
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
| Architecture | Biggest Problem |
|---|---|
| MVC | Massive Activities |
| MVP | Boilerplate |
| MVVM | Excellent balance |
| MVI | More 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 Case | Recommended |
|---|---|
| Screen UI State | StateFlow |
| One-time events | SharedFlow |
| Legacy XML apps | LiveData (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.
Part 9 — Recommended Project Structure
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
- Why do Android apps need an architecture?
- What problems do Massive Activities cause?
- Explain the SOLID principles with Android examples.
- Compare MVC, MVP, MVVM, and MVI.
- Why is MVVM Google’s recommended architecture?
- Why does a ViewModel survive configuration changes?
- When would you use
StateFlowvsSharedFlow? - What is the Repository pattern, and why is it useful?
- Explain the layers of Clean Architecture.
- 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, andSharedFlowinternals- 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.