Practical Clean Architecture for Android
By Vivek Patel · Updated
Clean Architecture makes dependency direction explicit. The domain contains contracts and business rules; data implements those contracts; presentation renders state and triggers operations. A single Gradle module can enforce these conventions before a larger team needs separate modules.
How the pieces connect
An illustrative folder structure
This structure explains the architecture. Exact generated paths are shown in the source walkthrough below.
app/src/main/java/com/example/app/
domain/
model/
repository/
usecase/
data/
remote/
local/
repository/
presentation/
di/Design repository contracts around business needs
Prefer observeSavedArticles over exposing a Room DAO directly. The contract should describe the operation and its domain values. Let the implementation choose between cache and network. Keep Retrofit response types and database entities inside data unless there is a deliberate reason to share them.
Put transaction rules in the right layer
A use case may coordinate validation and several repositories, but a database transaction belongs with the implementation that owns the database. Document consistency expectations in the contract. Offline synchronization needs explicit conflict handling; a folder layout cannot supply it.
Keep the composition root concrete
Hilt modules or a manual AppContainer create concrete implementations and bind interfaces. Presentation receives the domain contract. The starter uses a manual ViewModel factory with repository injection so clean boundaries remain visible even when a DI framework is selected.
Split modules when the team needs enforcement
Package boundaries are conventions within one module. Separate domain, data, and presentation modules when build isolation, ownership, or enforcement justifies the cost. Start small, test important rules, and resist copying the same model into every layer without a meaningful transformation.
Inside the generated starter
Inspect the domain contract and use case generated by the Clean + Hilt + Room preset. Room scaffolding and the demo counter are separate: choosing a database does not automatically persist every feature.
These source files are produced from the same templates used by the generator, with the configuration below. They are starter examples; the observations identify work still needed for a real feature.
app/src/main/java/com/example/myandroidapp/domain/repository/CounterRepository.kt
package com.example.myandroidapp.domain.repository
interface CounterRepository { suspend fun load(): Int; suspend fun save(value: Int) }
app/src/main/java/com/example/myandroidapp/domain/usecase/IncrementCounter.kt
package com.example.myandroidapp.domain.usecase
import com.example.myandroidapp.domain.repository.CounterRepository
class IncrementCounter(private val repository: CounterRepository) { suspend operator fun invoke(): Int { val next = repository.load() + 1; repository.save(next); return next } }
What to notice
IncrementCounter depends only on the Kotlin repository interface. In the Clean Architecture variant, HomeViewModel invokes that use case. The use case itself has no Android, Room, or Retrofit imports.
The starter uses packages within one application module. These boundaries are conventions; Gradle does not prevent a domain file from importing data. Extract a separate domain module if compile-time enforcement is worth the build complexity.
The load-then-save operation is not an atomic database increment. When concurrent writers matter, express an atomic operation in the repository contract and implement it transactionally in the data layer. Moving the same two calls into a use case does not solve the race.
Extend it and verify the result
Replace the counter repository with a Room-backed implementation. Keep DAO and entity types inside data, bind the implementation at the composition root, and test process recreation. Then simulate two simultaneous increments and decide whether the repository must guarantee atomicity.
- Domain code imports no Android or data implementation classes.
- The persisted count survives process recreation.
- Concurrent increments meet the contract instead of losing an update.
- Repository tests cover database behavior separately from use-case rules.
These are exercises to add to your application tests, not claims that the starter already handles every case.
Try this configuration
Clean Architecture · Hilt · Retrofit · Room · Compose
The generator includes a working counter example with a repository contract, selected dependencies, navigation, and local setup instructions. Extend the example around your own domain before shipping.
Configure your Android project →For prerequisites, exact setup commands, expected behavior, and build troubleshooting, follow the android download-to-first-run walkthrough. Check the validation record for the configurations actually checked.
Continue reading
Primary reference
Android domain layer guidance. The source walkthrough above describes AppScaffold’s implementation; platform documentation describes the broader APIs and recommendations.