Android & Compose Architecture Standards
Use this skill for Android work built with Kotlin, Compose, and related modern app architecture patterns.
1. Jetpack Compose UI Rules
- Model-View-Intent (MVI) & UDF:
- Model: Expose a single, immutable
ViewState (Data Class) from the ViewModel. Do not expose multiple independent state flows unless strictly isolated.
- View: A pure function rendering the Model. State flows down.
- Intent: User actions are routed to the ViewModel as explicit Intents/Events. Events flow up.
- Screen vs View Approach:
<Name>Screen: Handles DI, Navigation3 KMP routing, and connects the ViewModel to the UI.
<Name>View(state, onEvent): A completely pure, stateless Composable.
- Minimize Logic in Compose:
- Do NOT use
remember for business logic. Business logic belongs in the ViewModel.
- Performance:
- Assume Strong Skipping Mode is enabled. Do not manually wrap lambdas in
remember.
- Pass modifier chains explicitly:
modifier: Modifier = Modifier.
- Visibility:
@Preview Composables MUST be private or restricted visibility.
- Do NOT make Composable functions
public unless intended as an external design system component.
2. Navigation3 KMP (Routing & State)
If using Navigation3 KMP for architecture:
- ViewModel = Logic + State: The ViewModel handles all business logic, manages the CoroutineScope, and exposes state to the UI.
- ViewModel Interface:
- Public functions inside a ViewModel MUST represent user intents/events and generally return
Unit.
- Any function computing values internally should be
private.
- State should be exposed as an immutable
StateFlow.
- Routing (Navigation3 KMP):
- Treat navigation strictly as state management (part of the ViewModel or Component).
- Keep navigation execution strictly inside the
<Name>Screen wrapper, NOT inside the pure <Name>View.
- Use strongly typed destinations (Objects or Data Classes).
- Crucial: Apply polymorphic
@Serializable annotations to your destination keys so they serialize correctly across non-JVM platforms (iOS/Web).
3. Dependency Injection (Koin)
If using Koin:
- Prefer
single and factory instead of bind with generic provider/singleton blocks for clearer syntax and safety.
4. Project Structure & Layering
Prefer a clear 3-layer layout and keep boundaries strict:
data/: implementations (local DB, remote APIs, repository impls)
domain/: pure business logic (models, repository interfaces, use cases)
ui/: Compose screens, reusable components, theme
di/: DI wiring only (no business logic)
Rule: UI depends on domain, domain depends on nothing, data depends on domain (interfaces).
5. Coroutines, Flow, and State
- UI state:
- Use
MutableStateFlow internally and expose StateFlow via asStateFlow().
- Update state via
update { it.copy(...) } to keep changes atomic.
Example (micro):
private val _state = MutableStateFlow(ViewState())
val state: StateFlow<ViewState> = _state.asStateFlow()
_state.update { it.copy(isLoading = true) }
- Long-running streams:
- Use
flowOn(Dispatchers.IO) for data layer work.
- Prefer injecting a
CoroutineDispatcher for testability.
- Error handling:
- Catch at the right boundary (usually data/usecase) and surface a user-safe error state.
6. Result Modeling (Optional)
For operations that can be loading/success/error, prefer a sealed result type over nullable juggling.
Rule: keep it small (Loading, Success(data), Error(exception)), and map/transform explicitly.
7. Testing (Unit-first)
Recommended stack (when it fits the repo):
kotlinx-coroutines-test for deterministic coroutine tests
MockK for mocking
Turbine for testing Flow/StateFlow emissions
Rules:
- Tests must be deterministic (no real network, no timing races).
- Prefer injecting dispatchers and using test dispatchers.
- For flows: assert emission order and terminal states, not implementation details.
Example (micro):
@Test fun emitsLoadingThenData() = runTest {
// collect state/flow and assert emissions (use Turbine if available)
}
8. Lint + CI (Keep It Boring)
- Run static checks in CI:
- detekt (complexity/style)
- ktlint (formatting)
- unit tests
- Keep thresholds explicit (e.g., long method/parameter limits) and fail CI on violations.
- If the repo uses GitHub Actions, keep Android CI minimal:
- checkout
- JDK setup
- Gradle cache
- detekt/ktlint
- unit tests
- assemble (optional)
1---2name: android3description: Architecture, Jetpack Compose, Navigation3 KMP, and Koin DI rules for Android apps.4---56# Android & Compose Architecture Standards78Use this skill for Android work built with Kotlin, Compose, and related modern app architecture patterns.910## 1. Jetpack Compose UI Rules11121. **Model-View-Intent (MVI) & UDF**:13 - **Model**: Expose a single, immutable `ViewState` (Data Class) from the ViewModel. Do not expose multiple independent state flows unless strictly isolated.14 - **View**: A pure function rendering the Model. State flows down.15 - **Intent**: User actions are routed to the ViewModel as explicit Intents/Events. Events flow up.162. **Screen vs View Approach**:17 - `<Name>Screen`: Handles DI, Navigation3 KMP routing, and connects the `ViewModel` to the UI.18 - `<Name>View(state, onEvent)`: A completely pure, stateless Composable.193. **Minimize Logic in Compose**:20 - Do NOT use `remember` for business logic. Business logic belongs in the ViewModel.214. **Performance**:22 - Assume **Strong Skipping Mode** is enabled. Do not manually wrap lambdas in `remember`.23 - Pass modifier chains explicitly: `modifier: Modifier = Modifier`.245. **Visibility**:25 - `@Preview` Composables MUST be `private` or restricted visibility.26 - Do NOT make Composable functions `public` unless intended as an external design system component.2728## 2. Navigation3 KMP (Routing & State)2930If using Navigation3 KMP for architecture:31321. **ViewModel = Logic + State**: The ViewModel handles all business logic, manages the CoroutineScope, and exposes state to the UI.332. **ViewModel Interface**:34 - Public functions inside a ViewModel MUST represent user intents/events and generally return `Unit`.35 - Any function computing values internally should be `private`.36 - State should be exposed as an immutable `StateFlow`.373. **Routing (Navigation3 KMP)**:38 - Treat navigation strictly as **state management** (part of the ViewModel or Component).39 - Keep navigation execution strictly inside the `<Name>Screen` wrapper, NOT inside the pure `<Name>View`.40 - Use strongly typed destinations (Objects or Data Classes).41 - **Crucial**: Apply polymorphic `@Serializable` annotations to your destination keys so they serialize correctly across non-JVM platforms (iOS/Web).4243## 3. Dependency Injection (Koin)4445If using Koin:4647- Prefer `single` and `factory` instead of `bind` with generic provider/singleton blocks for clearer syntax and safety.4849## 4. Project Structure & Layering5051Prefer a clear 3-layer layout and keep boundaries strict:5253- `data/`: implementations (local DB, remote APIs, repository impls)54- `domain/`: pure business logic (models, repository interfaces, use cases)55- `ui/`: Compose screens, reusable components, theme56- `di/`: DI wiring only (no business logic)5758Rule: UI depends on domain, domain depends on nothing, data depends on domain (interfaces).5960## 5. Coroutines, Flow, and State61621. **UI state**:63 - Use `MutableStateFlow` internally and expose `StateFlow` via `asStateFlow()`.64 - Update state via `update { it.copy(...) }` to keep changes atomic.6566Example (micro):6768```kotlin69private val _state = MutableStateFlow(ViewState())70val state: StateFlow<ViewState> = _state.asStateFlow()71_state.update { it.copy(isLoading = true) }72```732. **Long-running streams**:74 - Use `flowOn(Dispatchers.IO)` for data layer work.75 - Prefer injecting a `CoroutineDispatcher` for testability.763. **Error handling**:77 - Catch at the right boundary (usually data/usecase) and surface a user-safe error state.7879## 6. Result Modeling (Optional)8081For operations that can be loading/success/error, prefer a sealed result type over nullable juggling.8283Rule: keep it small (`Loading`, `Success(data)`, `Error(exception)`), and map/transform explicitly.8485## 7. Testing (Unit-first)8687Recommended stack (when it fits the repo):8889- `kotlinx-coroutines-test` for deterministic coroutine tests90- `MockK` for mocking91- `Turbine` for testing `Flow`/`StateFlow` emissions9293Rules:94951. Tests must be deterministic (no real network, no timing races).962. Prefer injecting dispatchers and using test dispatchers.973. For flows: assert emission order and terminal states, not implementation details.9899Example (micro):100101```kotlin102@Test fun emitsLoadingThenData() = runTest {103 // collect state/flow and assert emissions (use Turbine if available)104}105```106107## 8. Lint + CI (Keep It Boring)1081091. Run static checks in CI:110 - detekt (complexity/style)111 - ktlint (formatting)112 - unit tests1132. Keep thresholds explicit (e.g., long method/parameter limits) and fail CI on violations.1143. If the repo uses GitHub Actions, keep Android CI minimal:115 - checkout116 - JDK setup117 - Gradle cache118 - detekt/ktlint119 - unit tests120 - assemble (optional)