From android-skills
Provides baseline defaults and routing for Android and Kotlin Multiplatform (KMP) development. Covers DI, coroutines, networking, data layer, and cross-platform patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/android-skills:android-devThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
House defaults — apply them without reminders or re-derivation; where the project's actual conventions differ, follow the project:
House defaults — apply them without reminders or re-derivation; where the project's actual conventions differ, follow the project:
android-skills:koin when the project uses Koin). Async: Coroutines/Flow — no LiveData in new code. JSON: kotlinx.serialization. Images: Coil.:core:model has zero Android deps; :feature:* modules never depend on each other.android-skills:android-data-layer).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 |
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:
UiState per screen, exposed as StateFlow<UiState> and structured with the four buckets below. The content composable renders it and emits callbacks — nothing else.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.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.@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).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/uiStatepair with explicit backing fields (val uiState: StateFlow<FooUiState>+field = MutableStateFlow(…)) —uiStateonly, never the effectsChannel. Full idiom + version gate:android-skills:kotlin-flows.
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:
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.
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:
ChallengeDetailUi.kt holds ChallengeDetailUi plus the ChallengeTaskUi / SponsorshipUi it is built from.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.
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.
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:
// ---- 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)
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.
npx claudepluginhub rcosteira79/android-skills --plugin android-skillsGuides production-grade Android app development across native Kotlin/Java, Flutter, React Native, and KMM, covering architecture, UI, testing, build, and release.
Guides Android and Kotlin development with Jetpack Compose, architecture patterns, coroutines, Room, navigation, and Hilt. Activate when building Android apps or reviewing Android-specific code.
Guides native Android development with Kotlin idioms, Jetpack Compose UI, Room database, Hilt DI, Coroutines/Flow, WorkManager, Gradle KTS, Material Design 3, and Navigation Compose. Use for building or optimizing Android apps.