Kotlin
Kotlin / Android.
What this is
Kotlin / Android. Coroutine structure and lifetime dominate — GlobalScope launches that outlive their owner, blocking calls inside suspend functions, broad catches that also swallow CancellationException and break structured cancellation, mutable state shared across coroutines without confinement. The rest is Android-specific state discipline: Activity Context leaked by a singleton, mutable StateFlow/LiveData exposed publicly instead of read-only, force-unwrap (!!) in production paths.
How to onboard it
A repository picks up this stack by onboarding on a profile that includes it. redline init detects the profile from what is in the repository, so in most cases this is automatic:
$ npx redlinegate init # detects the profile $ npx redlinegate init --profile mobile-android # or name one
One profile pulls these rules in: mobile-android.
Once onboarded, your files match this stack when they fit any of these globs:
**/*.kt**/*.kts
How to use it — 17 rules
Nothing to run. Once your profile includes Kotlin, redline init renders these rules into your repository's AI tooling and the reviewer applies them on every pull request. When a review comment cites one of these ids, this table is where to look up what it catches and why.
| Rule id | Severity | Catches |
|---|---|---|
kotlin/force-unwrap | BLOCKER | `!!` non-null assertions in production paths — restructure with `?.`, `?:`, `requireNotNull` with a message, or fix the type. |
kotlin/globalscope | BLOCKER | `GlobalScope.launch` — coroutines must live in a structured scope (`viewModelScope`, `lifecycleScope`, injected `CoroutineScope`); GlobalScope leaks work past the owner's lifetime. |
kotlin/blocking-in-coroutine | BLOCKER | Blocking inside coroutines: `Thread.sleep`, blocking IO, or `runBlocking` on `Dispatchers.Main` / inside `suspend` functions — use `delay`, suspending clients, or `withContext(Dispatchers.IO)`. |
kotlin/context-leak | BLOCKER | Android `Context` held by singletons / companion objects — activity context leaks the whole view tree; application context only, and only when unavoidable. |
kotlin/broad-catch-cancellation | BLOCKER | Catching `Exception`/`Throwable` broadly and continuing — also swallows `CancellationException`, breaking coroutine cancellation; catch specific types and rethrow `CancellationException`. |
kotlin/unconfined-shared-state | BLOCKER | Mutable shared state across coroutines without confinement — use `Mutex`, `StateFlow`, or single-thread confinement. |
kotlin/lateinit-misuse | BLOCKER | `lateinit` used to dodge nullability logic — acceptable only for framework-injected fields (DI, Android lifecycle). |
kotlin/public-mutable-state | HIGH | Public `MutableStateFlow` / `MutableLiveData` — expose read-only `StateFlow`/`LiveData`, mutate privately. |
kotlin/lifecycle-unaware-collection | HIGH | Flow collection in UI without `repeatOnLifecycle` / `collectAsStateWithLifecycle` — collects while backgrounded. |
kotlin/hardcoded-dispatcher | HIGH | Hardcoded `Dispatchers.*` in classes — inject dispatchers for testability. |
kotlin/data-class-var | HIGH | `data class` with `var` properties — copy/equality semantics break; use `val` + `copy`. |
kotlin/companion-mutable-state | HIGH | Companion-object mutable state (global by another name). |
kotlin/runcatching-silent-default | HIGH | `runCatching` chains that map failure to a default silently. |
kotlin/force-unwrap-in-tests | HIGH | `!!` in tests hiding what the test actually asserts — use `assertNotNull` semantics. |
kotlin/prefer-sealed-state | SUGGESTION | Sealed interfaces/classes for UI and result states instead of nullable-field combinations. |
kotlin/value-class-identifiers | SUGGESTION | `value class` for domain identifiers (MSISDN, AccountId) instead of raw `String`. |
kotlin/deep-scope-function-nesting | SUGGESTION | Nested `let`/`apply`/`run` chains more than two deep — extract a function. |
Expected output
A finding from this file, and every finding Redline produces, opens with a machine-readable first line — severity, then the rule id in brackets, then the problem in one line:
Redline/BLOCKER [kotlin/force-unwrap]: <one-line problem>Ids are aggregated per rule, which is how the organisation finds out which rules earn their place and which only generate noise — so a finding without a valid id cannot be measured and counts as untagged. Of the 17 rules here, 7 BLOCKER, 7 HIGH and 3 SUGGESTION. Only a BLOCKER must not merge; a SUGGESTION may be dismissed without justification, and is never upgraded to get attention.
If a rule here fires constantly on code your team has deliberately decided to allow, that is the signal to raise with the standards owner — the rule id is what makes that conversation measurable — not to argue it away comment by comment.
How to edit it
Rules are edited in standards/stacks/kotlin.md and nowhere else. The rendered copies in AGENTS.md, .github/copilot-instructions.md and .github/instructions/ are generated and are overwritten by the next render. A change here propagates to every onboarded repository as a pull request, so treat it as a production change.
- Edit the markdownstandards/ is the only place a human edits a rule. Everything under AGENTS.md, .github/copilot-instructions.md and .github/instructions/ is rendered from it and is overwritten by the next render.
- node scripts/assign-rule-ids.mjsAssigns a permanent <stack>/<slug> id to any new rule bullet and rewrites the file in place. Do not invent an id by hand. CI runs the same script with --check and fails if a rule is missing one.
- node scripts/render-self.mjsRe-renders this repository's own artifacts from the edited source. CI runs it with --check, so stale checked-in output fails the build.
- Bump standards/manifest.json → versionRequired in the same pull request as the rule change. Sync pull requests quote the version, so a repository's rendered artifacts always name where they came from.
- Add a CHANGELOG.md entryAlso in the same pull request. A standards change with no measurement is an opinion — record the seed score alongside it.
- node scripts/validate.mjsThe bundle self-check CI runs: manifest integrity, well-formed rule ids, the severity output contract surviving your edit, glob portability.
Rule ids are permanent. Rewording a rule is fine and keeps its id; renaming or removing an id orphans every historical telemetry record that cited it.
The full file
# Kotlin Review Rules (Android + backend) ## BLOCKER — request changes - `kotlin/force-unwrap` — **`!!` non-null assertions** in production paths — restructure with `?.`, `?:`, `requireNotNull` with a message, or fix the type. - `kotlin/globalscope` — **`GlobalScope.launch`** — coroutines must live in a structured scope (`viewModelScope`, `lifecycleScope`, injected `CoroutineScope`); GlobalScope leaks work past the owner's lifetime. - `kotlin/blocking-in-coroutine` — **Blocking inside coroutines**: `Thread.sleep`, blocking IO, or `runBlocking` on `Dispatchers.Main` / inside `suspend` functions — use `delay`, suspending clients, or `withContext(Dispatchers.IO)`. - `kotlin/context-leak` — **Android `Context` held by singletons / companion objects** — activity context leaks the whole view tree; application context only, and only when unavoidable. - `kotlin/broad-catch-cancellation` — **Catching `Exception`/`Throwable` broadly and continuing** — also swallows `CancellationException`, breaking coroutine cancellation; catch specific types and rethrow `CancellationException`. - `kotlin/unconfined-shared-state` — **Mutable shared state across coroutines without confinement** — use `Mutex`, `StateFlow`, or single-thread confinement. - `kotlin/lateinit-misuse` — **`lateinit` used to dodge nullability logic** — acceptable only for framework-injected fields (DI, Android lifecycle). ## HIGH - `kotlin/public-mutable-state` — Public `MutableStateFlow` / `MutableLiveData` — expose read-only `StateFlow`/`LiveData`, mutate privately. - `kotlin/lifecycle-unaware-collection` — Flow collection in UI without `repeatOnLifecycle` / `collectAsStateWithLifecycle` — collects while backgrounded. - `kotlin/hardcoded-dispatcher` — Hardcoded `Dispatchers.*` in classes — inject dispatchers for testability. - `kotlin/data-class-var` — `data class` with `var` properties — copy/equality semantics break; use `val` + `copy`. - `kotlin/companion-mutable-state` — Companion-object mutable state (global by another name). - `kotlin/runcatching-silent-default` — `runCatching` chains that map failure to a default silently. - `kotlin/force-unwrap-in-tests` — `!!` in tests hiding what the test actually asserts — use `assertNotNull` semantics. ## SUGGESTION - `kotlin/prefer-sealed-state` — Sealed interfaces/classes for UI and result states instead of nullable-field combinations. - `kotlin/value-class-identifiers` — `value class` for domain identifiers (MSISDN, AccountId) instead of raw `String`. - `kotlin/deep-scope-function-nesting` — Nested `let`/`apply`/`run` chains more than two deep — extract a function.