Android & Kotlin — Enterprise Production Skill (2026)
Authoritative Android engineering kit. Real patterns from AnimatedClockJetpacl, Authenticator, RepLock.
Not an Android library, voice-assistant skill, on-device LLM SDK, or Compose UI library.
This is markdown for coding AIs — clone into .cursor/skills/ or .claude/skills/. See README § "What this is NOT".
Meta index: AGENTS.md · CI: python3 scripts/validate_skills.py
Sub-Skills (Modular)
| Skill |
Path |
Scope |
| Architecture |
skills/android-kotlin-architecture/SKILL.md |
MVI/MVVM, modules, UseCases, UDF |
| Compose UI |
skills/android-kotlin-compose/SKILL.md |
UI, edge-to-edge, performance, motion |
| Testing |
skills/android-kotlin-testing/SKILL.md |
Turbine, Compose tests, Hilt fakes |
2026 Toolchain — Non-Negotiable
| Tool |
Minimum |
Notes |
| Kotlin |
2.0+ (2.1.x) |
K2 compiler only — no K1 fallback |
| AGP |
9.0+ |
Built-in Kotlin, new DSL defaults |
| Compose BOM |
2025.05+ |
Material 3, strong skipping default |
| Navigation |
3.x (2.9+ artifact) |
Type-safe NavKey, @Serializable routes |
| compileSdk / targetSdk |
35+ |
Edge-to-edge mandatory |
| JDK |
17 |
Required for AGP 9 |
[versions]
kotlin = "2.1.20"
agp = "9.0.0"
compose-bom = "2025.05.00"
navigation = "2.9.0"
room = "2.7.1"
hilt = "2.56.2"
lifecycle = "2.9.0"
coroutines = "1.10.2"
Kotlin 2.x / K2 Compiler Constraints
- Enable K2 —
kotlin.compiler.execution.strategy default; use ksp not kapt for Hilt/Room.
- Smart casts — prefer
when (val x = state) { is Loaded -> ... } over unsafe !!.
- Sealed hierarchy —
sealed interface UiState + data object Loading : UiState for exhaustive when.
@JvmInline value class for IDs (@JvmInline value class UserId(val raw: String)).
@Immutable / @Stable on UiState and UI models passed to Compose.
- Explicit API optional for libraries —
kotlin { explicitApi() } in published modules.
- No
!! except in tests with comment — use requireNotNull / early return.
data class copy for state — never mutate UiState fields in place.
sealed interface AccountsUiState {
data object Loading : AccountsUiState
data class Ready(
val accounts: List<AccountUi>,
val query: String = ""
) : AccountsUiState
data class Error(@StringRes val messageRes: Int) : AccountsUiState
}
MVI Guardrails — Atomic State Only
Allowed mutation: _state.update { it.copy(...) } or _state.update { prev -> ... }.
Banned in ViewModel:
// BANNED
_state.value = AccountsUiState(...) // naked assign
_state.value.accounts.add(item) // mutating list inside state
mutableStateOf / mutableStateListOf in VM // Compose scope only
viewModelScope.launch { _state.value = ... } // use update inside launch
Required pattern:
class AccountsViewModel @Inject constructor(
private val repository: AccountsRepository
) : ViewModel() {
private val _state = MutableStateFlow<AccountsUiState>(AccountsUiState.Loading)
val state: StateFlow<AccountsUiState> = _state.asStateFlow()
private val _effects = Channel<AccountsEffect>(Channel.BUFFERED)
val effects: Flow<AccountsEffect> = _effects.receiveAsFlow()
fun onEvent(event: AccountsEvent) {
when (event) {
AccountsEvent.Refresh -> refresh()
is AccountsEvent.QueryChanged -> _state.update { current ->
when (current) {
is AccountsUiState.Ready -> current.copy(query = event.query)
else -> current
}
}
}
}
private fun refresh() {
viewModelScope.launch {
_state.update { AccountsUiState.Loading }
repository.load()
.onSuccess { list ->
_state.update { AccountsUiState.Ready(accounts = list.map { it.toUi() }) }
}
.onFailure {
_state.update { AccountsUiState.Error(R.string.error_load_failed) }
}
}
}
}
UI collects:
@Composable
fun AccountsRoute(vm: AccountsViewModel = hiltViewModel()) {
val state by vm.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
vm.effects.collect { effect -> /* one-shot: nav, snackbar */ }
}
AccountsScreen(state = state,
}
Navigation 3 + Edge-to-Edge
@Serializable data object Home
@Serializable data class Detail(val id: String)
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController, startDestination = Home) {
composable<Home> { HomeScreen(onOpen = { navController.navigate(Detail(it)) }) }
composable<Detail> { entry ->
val route = entry.toRoute<Detail>()
DetailScreen(id = route.id)
}
}
}
- Routes =
@Serializable types, not string paths
- Pass IDs in nav args — fetch models in destination ViewModel
enableEdgeToEdge() in Activity — see compose sub-skill
Deep dive: references/07-navigation.md
Banned Antipatterns — AI Hallucination Lookup
Full table with WRONG/RIGHT pairs and row numbers: references/00-banned-antipatterns.md
Load before reviewing any agent-generated Kotlin. Compose audit checklist: skills/android-kotlin-compose/SKILL.md § REVIEW MODE.
Reference Routing
| File |
Topic |
references/00-banned-antipatterns.md |
Master banned table — load first for reviews |
references/01-architecture.md |
Clean Arch, MVVM, modules |
references/02-compose-ui.md |
Composition, M3, theming |
references/03-animations.md |
Motion, Canvas |
references/04-coroutines-flow.md |
Flow, errors, cancellation |
references/05-hilt-di.md |
DI, scopes |
references/06-room-db.md |
Room, offline-first |
references/07-navigation.md |
Nav 3, deep links |
references/08-kmp-cmp.md |
KMP, expect/actual |
references/09-networking.md |
Ktor, JWT |
references/10-performance.md |
Recomposition, Coil, profiles |
references/11-testing.md |
Turbine, UI tests |
references/12-camera-mlkit.md |
CameraX, pose |
references/13-release-checklist.md |
R8, Play Store |
references/14-datastore.md |
DataStore, SP migration, encryption |
references/15-paging3.md |
Paging 3, RemoteMediator, asState() |
references/16-coil-image.md |
Coil 3.4, AsyncImage, cache |
references/17-accessibility.md |
TalkBack, semantics, WCAG |
references/18-gradle-build-logic.md |
Convention plugins, KSP, R8, baseline profiles |
references/19-xml-to-compose-migration.md |
XML → Compose, RxJava → Flow |
Platforms
Target (what the docs teach): Android · Kotlin Multiplatform · Compose Multiplatform
Host (where the docs are read): macOS · Linux · Windows — anywhere your AI agent runs
Agent compatibility: 27 install guides → agents/README.md
Repo: https://github.com/haidrrrry/compose-kotlin-agent-skills
Mandatory Defaults (Summary)
- Strings:
stringResource / @StringRes — zero hardcoded UI text
- State: ViewModel
StateFlow + _state.update { }
- UI: Stateless composables,
modifier first optional param
- Lists:
LazyColumn + keys + contentType
- DI:
@HiltViewModel + constructor inject
- Tests: Fakes + Turbine — see testing sub-skill
Validation Before PR
python3 scripts/validate_skills.py --strict
Anti-Rationalizations
| Excuse |
Reality |
| "UiState overkill" |
data object Loading costs one line |
| "Keys later" |
Broken scroll/focus NOW |
| "Prototype skip repo" |
Prototypes ship |
| "collectAsState fine" |
Background collector drains battery |
Examples
| Path |
Patterns |
examples/animated-clock/ |
Canvas, rotate(), particles |
examples/authenticator/ |
Room Flow, combine, CompositionLocal theme |
1---2name: compose-kotlin-agent-skills3description: Enterprise Android/Kotlin skill for 2026 — Kotlin 2.x K2 compiler, AGP 9, Navigation 3, edge-to-edge Compose, strict MVI with atomic _state.update, banned AI antipatterns. Jetpack Compose, Hilt, Room, KMP, CameraX/ML Kit, performance, testing. Use when writing Kotlin for Android, building Compose UI, MVVM/MVI architecture, debugging recomposition, Room DAOs, Navigation 3, coroutines, or asking "structure Android app", "ViewModel UiState", "collectAsStateWithLifecycle", "edge to edge", "AGP 9", "K2 compiler", "Play Store release".4license: MIT5---67# Android & Kotlin — Enterprise Production Skill (2026)89Authoritative Android engineering kit. Real patterns from [AnimatedClockJetpacl](https://github.com/haidrrrry/AnimatedClockJetpacl), [Authenticator](https://github.com/haidrrrry/Authenticator), [RepLock](https://github.com/haidrrrry/RepLockPushupAppBlocker).1011> **Not** an Android library, voice-assistant skill, on-device LLM SDK, or Compose UI library.12> This is **markdown for coding AIs** — clone into `.cursor/skills/` or `.claude/skills/`. See README § "What this is NOT".1314**Meta index:** [`AGENTS.md`](AGENTS.md) · **CI:** `python3 scripts/validate_skills.py`1516## Sub-Skills (Modular)1718| Skill | Path | Scope |19|-------|------|-------|20| Architecture | [`skills/android-kotlin-architecture/SKILL.md`](skills/android-kotlin-architecture/SKILL.md) | MVI/MVVM, modules, UseCases, UDF |21| Compose UI | [`skills/android-kotlin-compose/SKILL.md`](skills/android-kotlin-compose/SKILL.md) | UI, edge-to-edge, performance, motion |22| Testing | [`skills/android-kotlin-testing/SKILL.md`](skills/android-kotlin-testing/SKILL.md) | Turbine, Compose tests, Hilt fakes |2324## 2026 Toolchain — Non-Negotiable2526| Tool | Minimum | Notes |27|------|---------|-------|28| Kotlin | **2.0+** (2.1.x) | K2 compiler only — no K1 fallback |29| AGP | **9.0+** | Built-in Kotlin, new DSL defaults |30| Compose BOM | **2025.05+** | Material 3, strong skipping default |31| Navigation | **3.x** (`2.9+` artifact) | Type-safe `NavKey`, `@Serializable` routes |32| compileSdk / targetSdk | **35+** | Edge-to-edge mandatory |33| JDK | **17** | Required for AGP 9 |3435```toml36[versions]37kotlin = "2.1.20"38agp = "9.0.0"39compose-bom = "2025.05.00"40navigation = "2.9.0"41room = "2.7.1"42hilt = "2.56.2"43lifecycle = "2.9.0"44coroutines = "1.10.2"45```4647## Kotlin 2.x / K2 Compiler Constraints48491. **Enable K2** — `kotlin.compiler.execution.strategy` default; use `ksp` not `kapt` for Hilt/Room.502. **Smart casts** — prefer `when (val x = state) { is Loaded -> ... }` over unsafe `!!`.513. **Sealed hierarchy** — `sealed interface UiState` + `data object Loading : UiState` for exhaustive `when`.524. **`@JvmInline value class`** for IDs (`@JvmInline value class UserId(val raw: String)`).535. **`@Immutable` / `@Stable`** on UiState and UI models passed to Compose.546. **Explicit API** optional for libraries — `kotlin { explicitApi() }` in published modules.557. **No `!!`** except in tests with comment — use `requireNotNull` / early return.568. **`data class` copy** for state — never mutate UiState fields in place.5758```kotlin59sealed interface AccountsUiState {60 data object Loading : AccountsUiState61 data class Ready(62 val accounts: List<AccountUi>,63 val query: String = ""64 ) : AccountsUiState65 data class Error(@StringRes val messageRes: Int) : AccountsUiState66}67```6869## MVI Guardrails — Atomic State Only7071**Allowed mutation:** `_state.update { it.copy(...) }` or `_state.update { prev -> ... }`.7273**Banned in ViewModel:**7475```kotlin76// BANNED77_state.value = AccountsUiState(...) // naked assign78_state.value.accounts.add(item) // mutating list inside state79mutableStateOf / mutableStateListOf in VM // Compose scope only80viewModelScope.launch { _state.value = ... } // use update inside launch81```8283**Required pattern:**8485```kotlin86class AccountsViewModel @Inject constructor(87 private val repository: AccountsRepository88) : ViewModel() {8990 private val _state = MutableStateFlow<AccountsUiState>(AccountsUiState.Loading)91 val state: StateFlow<AccountsUiState> = _state.asStateFlow()9293 private val _effects = Channel<AccountsEffect>(Channel.BUFFERED)94 val effects: Flow<AccountsEffect> = _effects.receiveAsFlow()9596 fun onEvent(event: AccountsEvent) {97 when (event) {98 AccountsEvent.Refresh -> refresh()99 is AccountsEvent.QueryChanged -> _state.update { current ->100 when (current) {101 is AccountsUiState.Ready -> current.copy(query = event.query)102 else -> current103 }104 }105 }106 }107108 private fun refresh() {109 viewModelScope.launch {110 _state.update { AccountsUiState.Loading }111 repository.load()112 .onSuccess { list ->113 _state.update { AccountsUiState.Ready(accounts = list.map { it.toUi() }) }114 }115 .onFailure {116 _state.update { AccountsUiState.Error(R.string.error_load_failed) }117 }118 }119 }120}121```122123**UI collects:**124125```kotlin126@Composable127fun AccountsRoute(vm: AccountsViewModel = hiltViewModel()) {128 val state by vm.state.collectAsStateWithLifecycle()129 LaunchedEffect(Unit) {130 vm.effects.collect { effect -> /* one-shot: nav, snackbar */ }131 }132 AccountsScreen(state = state, onEvent = vm::onEvent)133}134```135136## Navigation 3 + Edge-to-Edge137138```kotlin139@Serializable data object Home140@Serializable data class Detail(val id: String)141142@Composable143fun AppNavHost(navController: NavHostController = rememberNavController()) {144 NavHost(navController, startDestination = Home) {145 composable<Home> { HomeScreen(onOpen = { navController.navigate(Detail(it)) }) }146 composable<Detail> { entry ->147 val route = entry.toRoute<Detail>()148 DetailScreen(id = route.id)149 }150 }151}152```153154- Routes = `@Serializable` types, not string paths155- Pass **IDs** in nav args — fetch models in destination ViewModel156- `enableEdgeToEdge()` in Activity — see compose sub-skill157158Deep dive: [`references/07-navigation.md`](references/07-navigation.md)159160## Banned Antipatterns — AI Hallucination Lookup161162Full table with WRONG/RIGHT pairs and row numbers: **[`references/00-banned-antipatterns.md`](references/00-banned-antipatterns.md)**163164Load before reviewing any agent-generated Kotlin. Compose audit checklist: [`skills/android-kotlin-compose/SKILL.md`](skills/android-kotlin-compose/SKILL.md) § REVIEW MODE.165166## Reference Routing167168| File | Topic |169|------|-------|170| `references/00-banned-antipatterns.md` | Master banned table — load first for reviews |171| `references/01-architecture.md` | Clean Arch, MVVM, modules |172| `references/02-compose-ui.md` | Composition, M3, theming |173| `references/03-animations.md` | Motion, Canvas |174| `references/04-coroutines-flow.md` | Flow, errors, cancellation |175| `references/05-hilt-di.md` | DI, scopes |176| `references/06-room-db.md` | Room, offline-first |177| `references/07-navigation.md` | Nav 3, deep links |178| `references/08-kmp-cmp.md` | KMP, expect/actual |179| `references/09-networking.md` | Ktor, JWT |180| `references/10-performance.md` | Recomposition, Coil, profiles |181| `references/11-testing.md` | Turbine, UI tests |182| `references/12-camera-mlkit.md` | CameraX, pose |183| `references/13-release-checklist.md` | R8, Play Store |184| `references/14-datastore.md` | DataStore, SP migration, encryption |185| `references/15-paging3.md` | Paging 3, RemoteMediator, asState() |186| `references/16-coil-image.md` | Coil 3.4, AsyncImage, cache |187| `references/17-accessibility.md` | TalkBack, semantics, WCAG |188| `references/18-gradle-build-logic.md` | Convention plugins, KSP, R8, baseline profiles |189| `references/19-xml-to-compose-migration.md` | XML → Compose, RxJava → Flow |190191## Platforms192193**Target (what the docs teach):** Android · Kotlin Multiplatform · Compose Multiplatform194**Host (where the docs are read):** macOS · Linux · Windows — anywhere your AI agent runs195**Agent compatibility:** 27 install guides → [`agents/README.md`](agents/README.md)196197Repo: `https://github.com/haidrrrry/compose-kotlin-agent-skills`198199## Mandatory Defaults (Summary)200201- **Strings:** `stringResource` / `@StringRes` — zero hardcoded UI text202- **State:** ViewModel `StateFlow` + `_state.update { }`203- **UI:** Stateless composables, `modifier` first optional param204- **Lists:** `LazyColumn` + keys + `contentType`205- **DI:** `@HiltViewModel` + constructor inject206- **Tests:** Fakes + Turbine — see testing sub-skill207208## Validation Before PR209210```bash211python3 scripts/validate_skills.py --strict212```213214## Anti-Rationalizations215216| Excuse | Reality |217|--------|---------|218| "UiState overkill" | `data object Loading` costs one line |219| "Keys later" | Broken scroll/focus NOW |220| "Prototype skip repo" | Prototypes ship |221| "collectAsState fine" | Background collector drains battery |222223## Examples224225| Path | Patterns |226|------|----------|227| `examples/animated-clock/` | Canvas, `rotate()`, particles |228| `examples/authenticator/` | Room Flow, `combine`, CompositionLocal theme |