Clean Architecture: The Ultimate Guide for Modern Android
Table of Contents
🏛️ Philosophy: What is really “Clean”?
Clean Architecture, proposed by Robert C. Martin (Uncle Bob), is not a folder template; it is a software design philosophy focused on independence.
The ultimate goal is to create systems that are:
- Framework Independent: Android is a detail, not the center of your architecture.
- Testable: Business logic can be tested without UI, database, or web server.
- UI Independent: The UI can change easily without changing the rest of the system.
- Database Independent: You can switch from Room to Realm or SQLDelight without touching business logic.
🧅 The Dependency Rule
This is the only rule you cannot break.
“Source code dependencies can only point inwards, towards higher-level policies.”
Imagine the architecture like an onion:
- Center (Domain Layer): Entities and Pure Business Logic. Knows nothing about Android.
- Middle Layer (Data Layer / Adapters): Converts external data to the format the Domain needs.
- Outer Layer (Presentation / Framework): UI, Database, API, Android SDK.
The Domain never imports classes from the Data or Presentation layer. You will never see import android.* or import retrofit2.* in the Domain layer.
🏗️ Practical Implementation in Android
Let’s dissect each layer with a real example: A News App.
1. Domain Layer (The Sacred Core)
This layer contains the “Truth” of your application. It is pure Kotlin.
Components:
- Entities (Models): Pure business objects.
- Use Cases (Interactors): Application-specific business rules.
- Repository Interfaces: Contracts that the data layer must fulfill.
// Entity: Pure Kotlin, no JSON or DB annotations
data class NewsArticle(
val id: String,
val title: String,
val publishedAt: LocalDateTime
)
// Repository Interface: The Contract (Dependency Inversion)
interface NewsRepository {
fun getLatestNews(): Flow<Result<List<NewsArticle>>>
}
// Use Case: Logic Orchestrator
class GetLatestNewsUseCase @Inject constructor(
private val repository: NewsRepository
) {
operator fun invoke(): Flow<Result<List<NewsArticle>>> {
return repository.getLatestNews()
.map { result ->
// Business Rule: Filter future news (possible API error)
result.map { list ->
list.filter { it.publishedAt <= LocalDateTime.now() }
}
}
}
}
2. Data Layer (The Adapter)
This layer is the “plugin” that connects your Domain to the outside world.
Components:
- Data Models (DTOs): API (Retrofit) or DB (Room) models.
- Mappers: Transform DTO <-> Entity.
- Repository Implementation: Implements the Domain interface.
- Data Sources: Raw data sources.
// DTO: Network model with specific annotations
@JsonClass(generateAdapter = true)
data class NetworkNewsArticle(
@Json(name = "article_id") val id: String,
@Json(name = "header") val title: String
)
// Mapper: Extension function for conversion
fun NetworkNewsArticle.toDomain(): NewsArticle {
return NewsArticle(
id = this.id,
title = this.title,
publishedAt = LocalDateTime.now() // Simplified
)
}
// Repository Impl: This is where "Android" and libraries live
class NewsRepositoryImpl @Inject constructor(
private val api: NewsApiService,
private val dao: NewsDao
) : NewsRepository { // Implements Domain interface
override fun getLatestNews(): Flow<Result<List<NewsArticle>>> = flow {
// Cache logic, network, etc.
val apiResponse = api.fetchNews()
val domainNews = apiResponse.map { it.toDomain() }
emit(Result.success(domainNews))
}
}
3. Presentation Layer (The Face)
This layer is responsible for painting pixels on the screen.
Components:
- ViewModel: Maintains UI state and executes Use Cases.
- UI (Compose/XML): Observes ViewModel state.
@HiltViewModel
class NewsViewModel @Inject constructor(
private val getLatestNews: GetLatestNewsUseCase // Inject Use Case
) : ViewModel() {
// ViewModel does NOT know Repository, only Use Case
// ViewModel does NOT know Retrofit or Room
}
🔄 Control Flow vs. Dependency Flow
This is where many get confused.
- Control Flow (Runtime): UI -> ViewModel -> Use Case -> Repository Impl -> API.
- Dependency Flow (Compile time):
- Presentation -> Domain
- Data -> Domain
- Presentation -> Data (Only for dependency injection in Root/App module)
Thanks to Dependency Inversion (DIP), although control flow goes from Use Case to Repository Implementation, the source code dependency goes the other way: RepositoryImpl depends on RepositoryInterface (which is in Domain).
🧪 Benefits in Testing
By having the Domain isolated, testing Use Cases is trivial:
// Pure Kotlin test, runs in milliseconds on local JVM
class GetLatestNewsUseCaseTest {
private val fakeRepository = FakeNewsRepository() // In-memory Fake
private val useCase = GetLatestNewsUseCase(fakeRepository)
@Test
fun `should filter future news`() = runTest {
// Arrange
val futureArticle = NewsArticle("1", "Future", LocalDateTime.now().plusDays(1))
fakeRepository.emit(listOf(futureArticle))
// Act
val result = useCase().first()
// Assert
assertTrue(result.getOrThrow().isEmpty())
}
}
⚠️ Common Pitfalls
- Shared Anemic Models: Using the same object for DB, API, and UI.
- Why it’s bad: If you change the API, you break the UI. Violates layer separation.
- Pass-through Use Cases: Use Cases that only call the repository and do nothing else.
- Defense: Sometimes looks like boilerplate, but protects your architecture for when rules change. Still, if there is really no logic, some teams allow calling Repo directly from ViewModel (Pragmatic Clean Arch), but be careful.
- Business Logic in ViewModel: “If user is premium, show this”.
- Solution: Move that logic to the Use Case or Domain Entity.
🎯 Conclusion
Clean Architecture has an upfront cost: more files, more data mapping. But the return on investment is a codebase that survives time. Frameworks come and go (AsyncTask -> RxJava -> Coroutines -> ?), but your business logic, protected in the center of the onion, remains immutable.
You might also be interested in
MVVM Architecture in Android: The Comprehensive Guide (2025)
Master the Model-View-ViewModel pattern from basic concepts to advanced implementations with practical examples of a Minesweeper game for Android.
Kotlin Coroutines: The Android Guide
Mastering Kotlin Coroutines on Android. Dispatchers, structured concurrency, and best practices for asynchronous programming.
Android Documentation: Best Practices for Developers
Master the art of documenting Android projects. From KDoc to GitHub Actions, ensure your code is maintainable and scalable.