android-dev
>
Works with
---
name: android-dev
description: >
license: MIT
---
# Android / KMP baseline
House defaults — apply them without reminders or re-derivation; where the project's actual conventions differ, follow the project:
- **DI:** Hilt + KSP (or `android-skills:koin` when the project uses Koin). **Async:** Coroutines/Flow — no `LiveData` in new code. **JSON:** `kotlinx.serialization`. **Images:** Coil.
- **Network/local:** Android uses Retrofit/OkHttp + Room; KMP shared uses Ktor + Room or SQLDelight. Retrofit is Android-only — never in a shared module.
- **Modules:** feature-vertical packages and modules; `:core:model` has zero Android deps; `:feature:*` modules never depend on each other.
- **Errors:** mapped to a domain type at the repository boundary — platform exceptions never leak past it; UI state explicitly models loading / success / error (see `android-skills:android-data-layer`).
## Skill routing
Load the specific skill for the task, always with the **fully-qualified `android-skills:` prefix** — never the short name (`compose`, `koin`, …).
| For | Load |
|---|---|
| Compose detail — stability, `remember`, modifiers, side effects, lists, animation, navigation | `android-skills:compose` |
| M3 UX — touch targets, adaptive/foldable layouts, accessibility & M3-compliance audit | `android-skills:android-ux` |
| Coroutines & Flow — operators, `Channel` vs `SharedFlow`, structured concurrency | `android-skills:kotlin-coroutines`, `android-skills:kotlin-flows` |
| Repository / data layer + error model | `android-skills:android-data-layer` |
| Networking | `android-skills:android-retrofit` (Android) · `android-skills:kmp-ktor` (KMP) |
| Paging | `android-skills:paging` |
| Image loading | `android-skills:coil-compose` |
| Preferences / typed local storage | `android-skills:datastore` |
| KMP `expect`/`actual` boundary design | `android-skills:kmp-boundaries` |
| RxJava → Coroutines/Flow migration | `android-skills:rxjava-migration` |
| Testing | `android-skills:android-testing` |
| DI with Koin | `android-skills:koin` |
| Build logic / convention plugins | `android-skills:android-gradle-logic` |
| Build speed, kapt → KSP | `android-skills:gradle-build-performance` |
| Debugging — Logcat, crashes, ANRs, profiling | `android-skills:android-debugging` |
| AOSP / AndroidX source lookup | `android-skills:android-source-search` |
| Multi-module visibility & module boundaries | `android-skills:modularization` |
| Platform PDF annotation / page-object editing (API 36.1 / SDK ext 18) | `android-skills:pdf-annotations` |
## New-project UI convention (greenfield)
For a **new** project or feature with no established convention. In existing code, match what's already there — see *Reuse the project's existing mechanism* below.
The UI layer is MVVM with an MVI-style state/effect split:
- **One immutable `UiState` per screen**, exposed as `StateFlow<UiState>` and structured with the four buckets below. The content composable renders it and emits callbacks — nothing else.
- **One effects stream for fire-once imperatives** — navigate, snackbar/toast, scroll-to, share-sheet, haptics. Use `Channel(Channel.BUFFERED).receiveAsFlow()`, **not** `SharedFlow`: an effect emitted while the screen is backgrounded buffers and replays on resume instead of being dropped. Collect it in a `LaunchedEffect` (lifecycle-scoped via `repeatOnLifecycle`), never `collectAsStateWithLifecycle`. Channel-vs-`SharedFlow` rationale: `android-skills:kotlin-flows`.
- **State vs effect — "does it survive a config change?"** Anything still true after rotation / process death is **state** (an error to show = a field in `UiState`); anything the UI runs once and forgets is an **effect**. This is the `durable-state-over-events` rule in `compose/references/state-management.md`: keep durable things in state; the effect stream is only for one-shot imperatives.
- **Promote callbacks to a `@Stable Actions` interface at ~4–5+** (or when the same set is threaded through several composable layers). Below that, individual lambdas are simpler — don't abstract early. The ViewModel implements the interface; the content composable depends on `FooActions`, **never** the ViewModel, so it stays pure and previewable (a no-op `object : FooActions {}` in previews).
```kotlin
data class FooUiState(/* the four buckets — see below */)
sealed interface FooEffect {
data class NavigateTo(val id: String) : FooEffect
data class ShowSnackbar(val message: String) : FooEffect
}
@Stable // promote here once lambdas pile up (~4-5+)
interface FooActions {
fun onItemClick(id: String)
fun onRefresh()
}
class FooViewModel(/* … */) : ViewModel(), FooActions {
private val _uiState = MutableStateFlow(FooUiState())
val uiState: StateFlow<FooUiState> = _uiState.asStateFlow()
private val _effects = Channel<FooEffect>(Channel.BUFFERED) // not SharedFlow — buffers while backgrounded
val effects = _effects.receiveAsFlow()
override fun onItemClick(id: String) { /* _uiState.update { … } */ _effects.trySend(FooEffect.NavigateTo(id)) }
override fun onRefresh() { /* … */ }
}
@Composable
fun FooScreen(viewModel: FooViewModel = hiltViewModel(), onNavigate: (String) -> Unit) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val lifecycle = LocalLifecycleOwner.current.lifecycle
LaunchedEffect(Unit) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.effects.collect { effect ->
when (effect) {
is FooEffect.NavigateTo -> onNavigate(effect.id)
is FooEffect.ShowSnackbar -> { /* show snackbar */ }
}
}
}
}
FooContent(uiState = uiState, actions = viewModel) // VM passed as FooActions — FooContent sees only the interface
}
```
> **Kotlin 2.4+:** collapse the `_uiState`/`uiState` pair with explicit backing fields (`val uiState: StateFlow<FooUiState>` + `field = MutableStateFlow(…)`) — `uiState` only, never the effects `Channel`. Full idiom + version gate: `android-skills:kotlin-flows`.
## Four-bucket state modeling
Screens with rich interactions (forms, calculators, multi-step wizards) get unmanageable when state is one flat `data class`. Slice `UiState` into four explicit buckets, and **derive computed values as class properties, not constructor parameters**:
```kotlin
data class CheckoutUiState(
// 1. Editable input — what the user types
val email: String = "",
val cardNumber: String = "",
// 3. Persisted snapshot — last value read from the repository / stored cross-screen
val savedShippingAddress: Address? = null,
// 4. Transient UI-only — flags that must NOT survive the screen
val isSubmitting: Boolean = false,
val showCardScannerOverlay: Boolean = false,
) {
// 2. Derived — getters, NOT constructor params, so no caller can copy() into an
// inconsistent state (e.g. emailValid = false next to a valid email).
val emailValid: Boolean get() = email.isValidEmail()
val canSubmit: Boolean get() = emailValid && cardNumber.passesLuhn() && !isSubmitting
}
```
The bucket dictates lifecycle and persistence, not the field. Persisting `isSubmitting` keeps the spinner forever after process death; computing `canSubmit` outside the class lets it drift from the inputs; persisting `cardNumber` cross-screen leaks PII. Mixing the buckets produces bugs that look architectural.
## UI state and UI models live in different files
`UiState` is the screen's contract with its ViewModel, not a UI model — it gets its own file (or the ViewModel's, if that's the project's layout). Never sweep UI models into it. "Each type has a single owner / it's one unit of change / the real seam is role-per-file" argues for exactly this mistake: the state and the models it holds *are* different roles.
Group the models by **composition**, never by screen:
- Models composing one bigger model share that model's file, **named after the bigger model** — `ChallengeDetailUi.kt` holds `ChallengeDetailUi` plus the `ChallengeTaskUi` / `SponsorshipUi` it is built from.
- Independent models each get their own file.
- `FooModels.kt` / `FooUiModels.kt` is never the answer — reaching for a grab-bag name proves no aggregate root was found, which means the types are independent and belong in their own files.
Kotlin's "related declarations may share a file named after the primary declaration" is the mechanism, not a licence to nominate the *state* as that primary declaration and sweep the models in behind it.
## Reuse the project's existing mechanism
Before adding any new mechanism — an event dispatcher, an effects `Channel`/`SharedFlow`, a use-case layer, or a parallel state field — open a sibling ViewModel in the same feature and reuse what's already there. The easy miss here is **duplicating** an existing mechanism instead of widening it — adding a second `shouldDisplayUndoX` flag beside the existing one rather than generalizing the one that's there. If existing code contradicts a "best practice," follow the code and flag the inconsistency; never silently override the project's architecture.
## Comments — earn every one
**The test for every comment: could a reader quickly infer what it says from the code beside it? If yes, it's redundant — delete it.** A comment survives only by carrying what the code cannot: a non-obvious *why* — a decision, constraint, workaround, or gotcha. Never narrate *what* the code does; clear names and small functions already say it. "What a well-known type or call does" is a *what* the reader can look up, not a *why*.
Write the **fewest comments that pass that test** — this holds even when a task says "make it readable" or "for juniors." Readability comes from naming and structure; a comment a newcomer needs in order to follow *what* the code does is a signal to rename or extract, not to annotate.
**Keep** a genuine *why* (`// rethrow first — a broad catch would swallow CancellationException`), a justifying comment at a surprising call site, KDoc on a public API that adds information beyond its signature, and `TODO(owner-or-link)`. Honor an explicit request for documentation.
**Delete on sight** — each is trivially inferable from the code beside it:
```kotlin
// ---- domain model ---- // section-divider / banner (any width)
/** The user profile as the app cares about it. */ // KDoc restating the class name
val uiState = _uiState.asStateFlow() // private mutable, public read-only (restates the idiom)
} catch (e: IOException) { // no connectivity, timeout, DNS failure (restates what the type means)
```
## KMP
Inject a `CoroutineDispatcher` everywhere rather than calling `Dispatchers.Main` / `Dispatchers.IO` directly: `Dispatchers.Main` isn't guaranteed on every KMP target without the `-ktx` artifacts, and injection is also what makes dispatcher-swapped tests possible. Use `expect`/`actual` for platform specifics (file I/O, push tokens, biometrics); on iOS prefer immutable shared state.More Mobile skills
animation-vocabulary
emilkowalski/skills
Reverse-lookup glossary that turns a vague description of a web animation or motion effect into its exact term ("the bouncy thing when a popover opens" → Pop in; "the iOS rubber-band scroll" → Rubber-banding). Use when the user asks "what's it called when…", or describes a motion effect without knowing its name and wants the right word to prompt an AI or designer with. For naming an effect, not designing or building one.
cross-border-ecommerce
nexscope-ai/ecommerce-skills
Cross-border e-commerce expansion advisor. Scores target markets on 8 weighted dimensions (market size, ecommerce penetration, competition, regulatory complexity, logistics infrastructure, payment ecosystem, cultural distance, IP protection), compares 5 fulfillment models with cost and transit data, provides country-by-country tax/duty compliance guides (EU VAT/IOSS, UK VAT, US sales tax, CA GST, AU GST, JP consumption tax), maps local payment preferences by market, and builds a phased expansion roadmap. No API key required.
developing-genkit-dart
firebase/agent-skills
Generates code and provides documentation for the Genkit Dart SDK. Use when the user asks to build AI agents in Dart, use Genkit flows, or integrate LLMs into Dart/Flutter applications.

