Stack conventions
Android (Kotlin, Jetpack, Compose)
Android apps in Kotlin with Jetpack Compose, ViewModels and coroutines, built with Gradle and tested on the JVM and on devices.
Android apps in Kotlin with Jetpack Compose, ViewModels and coroutines, built with Gradle and tested on the JVM and on devices.
Used for: android.
Principles
- Follow unidirectional data flow: state flows down from the ViewModel to the UI, events flow up from the UI to the ViewModel.
- Layer the app as the architecture guide recommends: UI layer (Compose/Views + ViewModel), optional domain layer (use cases), data layer (repositories as the single source of truth over data sources).
- ViewModels expose immutable UI state as
StateFlow<UiState>and never hold references toContext,Activity,Viewor composables. - Collect flows in a lifecycle-aware way:
collectAsStateWithLifecycle()in Compose,repeatOnLifecycle(Lifecycle.State.STARTED)with Views. - Hoist state in Compose: composables receive state and lambdas; the lowest common owner holds it; stateless composables are previewable and testable.
- Do not block the main thread: all I/O and heavy work runs in suspend functions on injected dispatchers; repositories and data sources are main-safe.
- Survive configuration changes and process death: keep UI state in the ViewModel and use
SavedStateHandle/rememberSaveablefor what must be restored. - Use dependency injection (Hilt, or the repo's choice) rather than service locators or manual singletons.
- Build accessible UI: content descriptions, semantics, minimum 48dp touch targets, sufficient contrast, support for font scaling.
- Use the type system for null safety: non-null types by default,
?only where absence is meaningful; no!!except where a null is a proven programming error. - Prefer
valovervarand read-only collection types (List,Map) in APIs; expose mutable state only inside the owning class. - Model data with
data class, closed hierarchies withsealedclasses/interfaces, and handle them with exhaustivewhenexpressions (noelsebranch).
Conventions
Accessibility
- Give every meaningful icon/image a
contentDescription(null for decorative ones). - Use
Modifier.semantics/clickable(role = ...)so custom controls expose role and state to TalkBack. - Keep touch targets at least 48dp and support font scaling without clipping.
- Do not rely on colour alone; check contrast with the Accessibility Scanner or Compose UI checks.
Concurrency
- Use
viewModelScopefor screen work andlifecycleScopeonly for UI work; neverGlobalScope. - Expose
suspendfunctions andFlowfrom the data layer; do not exposeLiveDatain new code unless the repo standardises on it. - Every coroutine has an owner scope; cancel the scope when the owner ends.
- Use
withContext(dispatcher)for blocking calls; never call blocking I/O onDispatchers.Defaultor the main thread. - Protect shared mutable state with
Mutex, atomic types or confinement; do not usesynchronizedaround suspension points.
Dependencies
- Use the Gradle version catalog and the Compose BOM to keep AndroidX and Compose versions aligned.
- Declare versions in the Gradle version catalog (
gradle/libs.versions.toml) and use the Kotlin Gradle DSL (build.gradle.kts). - Keep the Kotlin compiler, coroutines and serialization library versions compatible.
Documentation
- Provide
@Previewcomposables for screens and components, including dark theme and large font variants. - KDoc on public API with
@param/@return/@throwswhere they add information; Markdown in KDoc for examples. - Comment why, not what; keep the README's build, run and configuration steps current when they change.
Errors
- Represent loading, success, empty and error as part of
UiState; never surface raw exceptions to the user. - Map data-layer exceptions to domain errors in the repository.
- Throw exceptions for programming errors; model expected failures with sealed types or
Resultat boundaries. - Use
runCatchingonly where you then handle both outcomes, and never around suspend calls without rethrowingCancellationException.
Logging
- Use the repo's logger (Timber or
Logbehind a wrapper); strip verbose logs from release builds and never log personal data or tokens. - Use the repo's logger (SLF4J, kotlin-logging) with lazy or parameterised messages; never log secrets or personal data.
- Log structured key-value context (request id, entity id) at boundaries; keep DEBUG noise out of hot paths.
Naming
- Screens:
XxxScreencomposable,XxxViewModel,XxxUiState; composables that emit UI are PascalCase nouns, composables returning values are camelCase. - Resource names in snake_case with a type prefix (
ic_,bg_); string resources for every user-facing text. - Classes and objects in UpperCamelCase; functions, properties and locals in lowerCamelCase;
const valand top-level immutable constants in UPPER_SNAKE_CASE. - Backing properties for exposed read-only state use a leading underscore (
private val _state/val state). - File names match the single class they contain, or describe the contents for top-level functions (
StringExtensions.kt).
Performance
- Keep composables cheap: compute in the ViewModel, use
remember/derivedStateOffor expensive derived values, and provide stablekeys in lazy lists. - Pass stable, immutable types to composables to keep recomposition skippable.
- Ship baseline profiles and R8 minification for release builds where the project supports them.
- Prefer sequences (
asSequence()) for long chains over large collections; plain collection operations for small ones. - Avoid unnecessary allocations in hot paths (lambdas capturing state, boxing of nullable primitives); measure before optimising.
Security
- Declare
android:exportedexplicitly and validate data from incoming intents and deep links. - Do not ship secrets in the APK/AAB; keep keys server-side or in the Android Keystore.
- Enforce HTTPS with a network security config; no cleartext traffic in release builds.
- Commit
gradle.lockfileor dependency verification metadata and act on vulnerability reports for libraries that ship in the app. - Parameterise all database queries (Exposed DSL, JPA parameters, jOOQ bindings); never interpolate input into SQL strings.
- Validate external input at the boundary and convert it to domain types (value classes) once.
- Keep secrets in environment variables or a secret store.
- Never pass external input to
Runtime.exec,ProcessBuilderwith a shell, or script engines (injection).
Structure
- Modularise by feature (
:feature:orders) and layer (:core:data) when the project is multi-module; follow the repo's existing module graph. - Composable functions accept a
modifier: Modifier = Modifieras the first optional parameter and apply it to the root element. - Package structure follows directories; omit the common root package directory as the conventions allow when the repo does.
- Order class contents: properties and init blocks, secondary constructors, methods, companion object last.
- Keep visibility minimal (
private/internal); enable explicit API mode for libraries where the repo uses it.
Testing
- Unit-test ViewModels with a main-dispatcher test rule, fake repositories and
runTest. - Test composables with
createComposeRuleand semantics-based finders (onNodeWithText,onNodeWithContentDescription). - Prefer fakes over mocks for repositories and data sources.
- Unit tests (
src/test) run on the JVM; instrumented integration tests (src/androidTest) cover navigation, Room and anything needing the framework. - Name tests after the behaviour (
`shows error when login fails`()), one behaviour per test. - Done means tested: new or changed behaviour of a ViewModel, repository or screen ships with tests in the same change.
- Use JUnit 5 or kotlin.test with the repo's assertion library (AssertK, Kotest, AssertJ); MockK for Kotlin-friendly mocking where mocks are needed.
- Test coroutines with
runTestand aTestDispatcher; inject dispatchers so tests do not depend on real time.
Review checklist
- Does the screen follow UDF with an immutable UiState exposed as StateFlow?
- Is the ViewModel free of Context, Activity and View references?
- Are flows collected with collectAsStateWithLifecycle or repeatOnLifecycle?
- Are composables stateless where possible, with state hoisted to the right owner?
- Do composables take a
modifierparameter applied to the root? - Is all I/O off the main thread on injected dispatchers?
- Does state survive configuration change and process death where needed?
- Do lazy lists use stable keys?
- Are images and controls labelled for TalkBack with adequate touch targets?
- Are exported components and incoming intents handled safely?
- Are ViewModels and composables covered by tests with fakes and test dispatchers?
- Are user-facing strings in resources?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
android-lint |
lint | Android Lint | no |
detekt |
static | detekt | no |
ktlint |
lint | ktlint (Gradle plugin) | no |
spotless-gradle |
format | spotless | no |
gradle-check |
test | gradle check | no |
osv-scanner |
security | osv-scanner | no |
gitleaks |
security | gitleaks | no |