android · 3 min read

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

Presentation → Domain→ Data → Domain→ DI wires both

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.