Clean Architecture in Android — Student Friendly Tutorial
Clean Architecture
1. What is Clean Architecture?
Clean Architecture means dividing Android application code into separate layers.
Each layer has its own responsibility:
- UI layer displays data.
- Domain layer applies business rules.
- Data layer gets data from an API or database.
This makes the application:
- Easy to understand
- Easy to test
- Easy to maintain
- Easy to modify
For example, if Retrofit is replaced with another networking library, the UI and business logic should not need major changes.
2. Three Main Layers
flowchart TD
P["Presentation Layer<br/>Screen, ViewModel, UI State"]
D["Domain Layer<br/>UseCase, Repository Interface, Model"]
DA["Data Layer<br/>RepositoryImpl, API, Database"]
P --> D
DA --> D| Layer | Easy meaning | Contains |
|---|---|---|
| Presentation | What the user sees | Screen, ViewModel, UI State |
| Domain | What the application should do | UseCase, Model, Repository interface |
| Data | Where the data comes from | API, Room, RepositoryImpl |
Memory trick
Presentation = Show data
Domain = Apply rules
Data = Get data3. Complete Application Flow
Suppose we want to display products from an API.
flowchart TD
A["Product Screen"] --> B["Product ViewModel"]
B --> C["Get Products UseCase"]
C --> D["Product Repository"]
D --> E["Repository Implementation"]
E --> F["Retrofit API or Room Database"]Simple explanation
1. The screen asks the ViewModel for products.
2. The ViewModel calls the UseCase.
3. The UseCase asks the repository.
4. Repository implementation calls the API or database.
5. The result returns to the screen.
flowchart BT
API["API / Database"] --> RI["RepositoryImpl"]
RI --> UC["UseCase"]
UC --> VM["ViewModel"]
VM --> UI["Screen"]4. Presentation Layer
The Presentation layer handles everything related to the screen.
It contains:
- Activity
- Fragment
- Compose screen
- ViewModel
- UI state
flowchart LR
U["User Action"] --> S["Screen"]
S --> VM["ViewModel"]
VM --> US["UI State"]
US --> SUI State
UI State describes what should appear on the screen.
data class ProductUiState(
val isLoading: Boolean = false,
val products: List<Product> = emptyList(),
val error: String? = null
)The screen can be in one of these situations:
stateDiagram-v2
[*] --> Loading
Loading --> Success
Loading --> Error
Error --> Loading: RetryViewModel
class ProductViewModel(
private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
fun loadProducts() {
viewModelScope.launch {
val products = getProductsUseCase()
// Update UI state
}
}
}The ViewModel should:
- Receive user actions
- Call UseCases
- Update UI state
The ViewModel should not directly call Retrofit or Room.
5. Domain Layer
The Domain layer contains the important application rules.
It contains:
- Domain models
- Repository interfaces
- UseCases
It should contain simple Kotlin code and should not depend on Android.
flowchart TD
UC["UseCase<br/>Business operation"]
R["Repository Interface<br/>Required data operations"]
M["Domain Model<br/>Application data"]
UC --> R
UC --> MDomain Model
A Domain Model represents information used by the application.
data class Product(
val id: String,
val name: String,
val available: Boolean
)It should not contain Retrofit or Room annotations.
Repository Interface
interface ProductRepository {
suspend fun getProducts(): List<Product>
}It tells us what operation is needed, but not how it will be performed.
UseCase
A UseCase represents one application operation.
Examples:
- Login user
- Get products
- Validate beneficiary
- Make payment
- Calculate discount
class GetProductsUseCase(
private val repository: ProductRepository
) {
suspend operator fun invoke(): List<Product> {
return repository.getProducts()
.filter { it.available }
}
}Here, the business rule is:
Return only available products.
6. Data Layer
The Data layer gets or saves data.
The data can come from:
- Retrofit API
- Room database
- Firebase
- Cache
- Local storage
flowchart TD
RI["Repository Implementation"]
API["Retrofit API"]
DB["Room Database"]
C["Cache"]
RI --> API
RI --> DB
RI --> CRepository Implementation
class ProductRepositoryImpl(
private val api: ProductApi
) : ProductRepository {
override suspend fun getProducts(): List<Product> {
return api.getProducts().map { dto ->
Product(
id = dto.id,
name = dto.title,
available = dto.available
)
}
}
}The repository implementation decides whether data should come from the API, database or cache.
7. Repository Interface vs Repository Implementation
flowchart LR
UC["UseCase"] --> I["ProductRepository<br/>Interface"]
IM["ProductRepositoryImpl"] -. implements .-> I
IM --> API["API / Database"]| Repository Interface | Repository Implementation |
|---|---|
| Tells what to do | Tells how to do it |
| Located in Domain | Located in Data |
| Used by UseCase | Uses API or Database |
| Does not know Retrofit | Can use Retrofit or Room |
Example:
Interface: Give me products.
Implementation: I will get products from Retrofit.8. Most Important Dependency Rule
The Domain layer should not depend on the Presentation or Data layer.
flowchart TD
P["Presentation"] --> D["Domain"]
DA["Data"] --> DThis means:
- Presentation knows Domain.
- Data knows Domain.
- Domain does not know Presentation or Data.
Wrong Design
flowchart LR
U["UseCase"] --> API["Retrofit API"]The UseCase is now directly connected to Retrofit.
Correct Design
flowchart LR
U["UseCase"] --> R["Repository Interface"]
RI["RepositoryImpl"] -. implements .-> R
RI --> API["Retrofit API"]Now the UseCase does not care whether data comes from Retrofit, Room or Firebase.
9. DTO and Domain Model
The API model and application model can be different.
flowchart LR
API["API Response"] --> DTO["ProductDto"]
DTO --> MAP["Mapper"]
MAP --> DOMAIN["Product"]API model
data class ProductDto(
val product_id: String,
val product_title: String,
val is_available: Boolean
)Domain model
data class Product(
val id: String,
val name: String,
val available: Boolean
)Mapper
fun ProductDto.toDomain(): Product {
return Product(
id = product_id,
name = product_title,
available = is_available
)
}Why use a mapper?
If the API field changes, only the Data layer needs modification.
The Domain and Presentation layers remain protected.
10. Why Not Call Retrofit Directly from ViewModel?
Wrong approach
flowchart LR
VM["ViewModel"] --> API["Retrofit API"]class ProductViewModel(
private val api: ProductApi
) : ViewModel()Problems:
- ViewModel becomes connected to Retrofit.
- Business and UI logic can become mixed.
- Unit testing becomes difficult.
- Changing the API affects the ViewModel.
Better approach
flowchart LR
VM["ViewModel"] --> UC["UseCase"]
UC --> R["Repository"]
R --> API["API"]class ProductViewModel(
private val getProductsUseCase: GetProductsUseCase
) : ViewModel()The ViewModel simply says:
Give me products.
It does not care where the products come from.
11. Clean Architecture with MVVM
Clean Architecture and MVVM are different.
flowchart TD
CA["Clean Architecture"]
P["Presentation Layer<br/>MVVM"]
D["Domain Layer<br/>UseCase"]
DA["Data Layer<br/>RepositoryImpl"]
CA --> P
CA --> D
CA --> DA| MVVM | Clean Architecture |
|---|---|
| Organizes the UI layer | Organizes the complete application |
| Contains View and ViewModel | Contains Presentation, Domain and Data |
| Manages screen state | Separates business rules and data sources |
Simple answer
MVVM is normally used inside the Presentation layer of Clean Architecture.
12. Simple Package Structure
com.example.app
├── presentation
│ ├── ProductScreen.kt
│ ├── ProductViewModel.kt
│ └── ProductUiState.kt
│
├── domain
│ ├── Product.kt
│ ├── ProductRepository.kt
│ └── GetProductsUseCase.kt
│
└── data
├── ProductApi.kt
├── ProductDto.kt
└── ProductRepositoryImpl.kt13. Benefits
- Each class has a clear responsibility.
- Business logic is easy to test.
- API, database or UI can be changed more easily.
- Large projects are easier to manage.
- Code becomes reusable.
- Team members can work on different layers.
Small limitation
- Clean Architecture creates more classes and files.
- For a small application, use a simple version and avoid unnecessary layers or UseCases.
14. Common Mistakes
Mistake 1: Calling API from ViewModel
flowchart LR
VM["ViewModel"] --> API["API"]Use this flow instead:
flowchart LR
VM["ViewModel"] --> UC["UseCase"]
UC --> R["Repository"]
R --> API["API"]Mistake 2: Putting business logic in ViewModel
Wrong
The ViewModel calculates:
- Tax
- Discount
- Payment limit
- Stock availability
Correct
Put these rules inside a UseCase.
class CalculatePriceUseCase {
operator fun invoke(price: Double, discount: Double): Double {
return price - discount
}
}Mistake 3: Using Android Context in Domain
Wrong
class LoginUseCase(
private val context: Context
)Correct
class LoginUseCase(
private val repository: AuthRepository
)The Domain layer should remain simple Kotlin code.
Mistake 4: Passing DTO Directly to UI
Wrong
flowchart LR
API["API"] --> DTO["ProductDto"]
DTO --> UI["Screen"]Correct
flowchart LR
API["API"] --> DTO["ProductDto"]
DTO --> M["Product"]
M --> UI["Screen"]Mistake 5: Repository Interface in Data Layer
Recommended placement:
Domain → ProductRepository
Data → ProductRepositoryImplThe UseCase depends on the interface, not the implementation.
Mistake 6: Creating Too Many UseCases
Avoid:
GetUserNameUseCase
GetUserAgeUseCase
GetUserEmailUseCasePrefer:
GetUserProfileUseCaseCreate a UseCase for a meaningful business operation.
Mistake 7: Overengineering a Small Application
Not every small operation requires:
- A separate model
- Multiple mappers
- Many UseCases
- Several modules
Use only the structure required by the project.
15. Interview Questions and Answers
Q1. What is Clean Architecture?
Clean Architecture separates an Android application into Presentation, Domain and Data layers. It keeps business logic independent from the UI, API, database and Android framework.
Q2. What are its main layers?
- Presentation: Screen, ViewModel and UI State
- Domain: UseCase, Domain Model and Repository interface
- Data: RepositoryImpl, API, Database, DTO and Mapper
Q3. What is the responsibility of a UseCase?
A UseCase represents one business operation.
Examples:
- Login
- Get products
- Validate payment
- Calculate price
The ViewModel calls the UseCase, and the UseCase calls the repository.
Q4. Why is the repository interface in Domain?
The Domain layer defines what data it needs without knowing how that data is fetched.
The Data layer implements the interface using Retrofit, Room or Firebase.
Q5. Can ViewModel call Repository directly?
Yes, for a very small and simple operation it can.
Use a UseCase when:
- Business rules are involved
- The operation is reused
- The logic needs separate testing
- The application is large
Q6. Why should Domain not use Android Context?
Using Context connects business logic to Android. This makes the Domain layer harder to test and reuse.
Domain should normally contain pure Kotlin code.
Q7. What is the Dependency Rule?
Presentation and Data can depend on Domain, but Domain should not depend on Presentation or Data.
flowchart TD
P["Presentation"] --> D["Domain"]
DA["Data"] --> DQ8. Why do we use DTO and Domain models separately?
DTO represents the API response.
Domain Model represents the data needed by the application.
Separating them prevents API changes from affecting the complete application.
Q9. Is Clean Architecture the same as MVVM?
No.
MVVM organizes the Presentation layer. Clean Architecture organizes the complete application.
Q10. How does it improve testing?
The UseCase depends on a repository interface. During testing, we can replace the real repository with a fake repository.
flowchart LR
T["Unit Test"] --> U["UseCase"]
U --> F["Fake Repository"]No real API, database or Android device is required.
16. Final Interview Answer
Clean Architecture divides an Android application into Presentation, Domain and Data layers. The Screen communicates with the ViewModel, the ViewModel calls a UseCase, and the UseCase calls a Repository interface. The Data layer implements the repository using Retrofit, Room or another data source. The Domain layer remains independent of Android and external libraries. This makes the application easier to test, maintain and modify.
Final Memory Diagram
flowchart TD
S["Screen<br/>Show data"] --> VM["ViewModel<br/>Manage UI state"]
VM --> UC["UseCase<br/>Apply business rules"]
UC --> R["Repository<br/>Request data"]
R --> DS["API / Database<br/>Provide data"]Presentation shows data, Domain applies rules, and Data fetches or saves data.