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
LayerEasy meaningContains
PresentationWhat the user seesScreen, ViewModel, UI State
DomainWhat the application should doUseCase, Model, Repository interface
DataWhere the data comes fromAPI, Room, RepositoryImpl

Memory trick

Presentation = Show data
Domain = Apply rules
Data = Get data


3. 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 --> S

UI 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: Retry

ViewModel

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 --> M

Domain 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 --> C

Repository 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 InterfaceRepository Implementation
Tells what to doTells how to do it
Located in DomainLocated in Data
Used by UseCaseUses API or Database
Does not know RetrofitCan 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"] --> D

This 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
MVVMClean Architecture
Organizes the UI layerOrganizes the complete application
Contains View and ViewModelContains Presentation, Domain and Data
Manages screen stateSeparates 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.kt


13. 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   → ProductRepositoryImpl

The UseCase depends on the interface, not the implementation.


Mistake 6: Creating Too Many UseCases

Avoid:

GetUserNameUseCase
GetUserAgeUseCase
GetUserEmailUseCase

Prefer:

GetUserProfileUseCase

Create 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?

  1. Presentation: Screen, ViewModel and UI State
  2. Domain: UseCase, Domain Model and Repository interface
  3. 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"] --> D


Q8. 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.