Global Skill

Android
Clean Architecture

Patterns, structures and rules for building robust, testable, and maintainable Android/KMP applications.

3
Layers
5
References
12
Patterns
44KB
Total
GitHub Install

The 3 Layers

Each layer has a single responsibility. The golden rule: domain depends on nothing.

UI Layer
Presentation — Compose Screens + ViewModels
@Composable ViewModel UiState
▼ ▼ ▼
Domain Layer
Business — Pure models + Interfaces + UseCases
data class interface Flow
▲ ▲ ▲
Data Layer
Infrastructure — DTOs, Mappers, API, DB
Ktor Room Mapper
UIDomainData
Domain knows NOTHING about UI or Data

Building Blocks

Each pattern has a specific role. Combined together, they form a robust and testable architecture.

Domain Model

Pure Kotlin data class — zero framework dependencies. The business core of your app.

Repository Interface

Contract defined in domain. Implemented by the data layer. ViewModel only sees the interface.

UseCase

operator fun invoke() — encapsulates a business operation. Optional, used when logic is shared across ≥2 VMs.

Mapper

Converts DTO ↔ Domain. Two styles: object Mapper or extension functions. Never in the ViewModel.

Repository Impl

Concrete implementation using runCatching. Coordinates remote + local DataSources.

DTO

@Serializable data class mirroring the JSON API. Isolated in data/remote/dto/.

ViewModel

State holder injecting domain interfaces via Koin. Exposes a StateFlow<UiState>.

UiState

sealed interface: Loading | Success | Error. Always exhaustive in when.

Screen

Stateless composable. Receives data + callbacks. Separated from Route (connected to VM).

Koin Module

Wiring interface → impl at runtime. single for repos, viewModel for VMs.

Error Handling

Result<T> + runCatching or custom AppResult<T> sealed type for typed errors.

Fake Repository

Test double implementing the domain interface. No mocking framework — pure Fakes.

Concrete Examples

From domain model to ViewModel — each layer has a distinct style.

domain/model/Match.kt Domain
// Zero framework imports
data class Match(
    val homeTeam: Club,
    val awayTeam: Club,
    val homeScore: Int,
    val awayScore: Int,
    val status: MatchStatus,
)

enum class MatchStatus {
    NOT_STARTED, LIVE, FINISHED
}
data/remote/mapper/MatchMapper.kt Data
object MatchMapper {
    fun toDomain(dto: FixtureDto): Match {
        return Match(
            homeTeam = mapTeam(dto.teams.home),
            awayTeam = mapTeam(dto.teams.away),
            homeScore = dto.goals.home ?: 0,
            awayScore = dto.goals.away ?: 0,
            status = mapStatus(dto.status),
        )
    }
}
ui/viewmodel/MatchesViewModel.kt UI
class MatchesViewModel(
    private val repository: MatchRepository,
) : ViewModel() {

    val uiState = MutableStateFlow<UiState>(Loading)

    fun loadMatches(date: LocalDate) {
        viewModelScope.launch {
            repository.getMatches(date)
                .onSuccess { uiState.value = Success(it) }
                .onFailure { uiState.value = Error(it) }
        }
    }
}
di/AppModules.kt DI
val repositoryModule = module {
    single<MatchRepository> {
        MatchRepositoryImpl(get())
    }
    single<NewsRepository> {
        NewsRepositoryImpl(get())
    }
}

val viewModelModule = module {
    viewModel { MatchesViewModel(get()) }
    viewModel { NewsViewModel(get()) }
}

Unidirectional Flow

Events go down, data goes up. Never take shortcuts.

User Action
tap, scroll
Screen
@Composable
ViewModel
StateFlow
Repository
interface
DataSource
Ktor / Room
Domain Model
mapped return
UiState
Success(data)
Recompose
UI updated

Technologies

KMP-first stack — maximum shared code between Android and iOS.

Kotlin Multiplatform
Compose Multiplatform
Koin DI
Ktor Client
Room KMP
SQLDelight
Coroutines
Kotlin Flow
kotlinx.serialization
Coil 3
Turbine
Navigation Compose

Anti-patterns

What to absolutely avoid — and the right alternative.

❌ Don't✅ Do
Import data.* in UI layerImport only domain.model.*
Business logic in @ComposableExtract to ViewModel or UseCase
DTOs as domain modelsSeparate domain models + mappers
Hilt/Dagger in KMPKoin (multiplatform compatible)
Retrofit in KMPKtor Client
GlobalScopeviewModelScope or structured concurrency
Fat repository with all logicSplit into focused DataSources
ViewModel in screens/Separate viewmodel/ package
Android framework in domain/Domain = pure Kotlin only
try/catch without mapping errorsrunCatching or Result<T>

New Feature

Steps to follow for every new feature implementation.

Skill References

5 detailed reference documents to deep-dive into each topic.

architecture.md

3 layers, DataSource, offline-first, expect/actual, convention plugins

10.7 KB

compose-patterns.md

State hoisting, shimmer, performance, remember, theme tokens

4.5 KB

modularization.md

Single-module vs multi-module, KMP structure, dependency rules

3.5 KB

gradle-setup.md

Version catalog, Ktor/Retrofit config, bundles, build.gradle.kts

6.3 KB

testing.md

Fakes, ViewModel tests, Mapper tests, Turbine, runTest

4.8 KB

Install the Skill

Clone the repo and copy it into your IDE's skills folder.

One-liner

skills.sh (recommended)
npx skills add MedAmineTazarki/android-clean-architecture-skill
Works with Claude Code, Cursor, Copilot, Windsurf, Gemini, Antigravity, Cline, and more.

Step 2 — Copy

Gemini / Antigravity IDE
# Windows xcopy /E /I . "%USERPROFILE%\.gemini\config\skills\android-clean-architecture" # macOS / Linux cp -r . ~/.gemini/config/skills/android-clean-architecture
Claude Code
cp -r . ~/.claude/skills/android-clean-architecture
The skill auto-activates when you work on an Android/KMP project.

Step 3 — Use

Example prompts
"Create a settings feature module with Clean Architecture" "Add an offline-first Repository for matches" "Structure the data layer with DTOs and mappers" "Configure Koin for the new ViewModel"
The AI will automatically follow the skill's patterns to generate clean, consistent code.