| name | kotlin-styleguide |
| description | Use when writing, editing, or reviewing Kotlin (.kt) source in a dexpace project — enforces the dexpace Kotlin styleguide (non-null by default, sealed result types, scope functions, 60-line function cap). Also use before committing Kotlin, or when asked to review Kotlin against the styleguide. |
Kotlin styleguide
Extends the Kotlin official coding conventions and the Google Android Kotlin style guide; where they conflict, the official conventions win, except the recorded deviations (120-column line limit; 60-line function cap).
When this applies
Editing *.kt, or reviewing Kotlin. Priority: correctness > performance > developer experience.
Non-negotiables
- Data + functions:
data class for state, top-level/extension functions for transformations, sealed hierarchies for closed polymorphism. No open inheritance for reuse.
- Nullability is part of the type: resolve at the boundary with smart casts,
?:, ?., or requireNotNull. !! is a missing model, never a shortcut.
val over var; immutable List over MutableList. Mutability is the choice you have to type.
- Explicit over implicit: every dependency in the constructor or signature. No reflection-driven control flow, no global mutable state.
- Errors are values: sealed
Result<T, E> for expected failures; exceptions only for unrecoverable conditions; wrap at the boundary, never leak across layers.
- Pick the concurrency primitive deliberately: coroutines for most async code, others only when forced. Name the seam.
- Small functions, breathing room: 60-line hard cap, aim 15–30; blank lines between logical sections.
- Assert aggressively:
require for caller contracts, check for invariants, error(...) for unreachable branches. ≥2 assertions per function on average; pair-assert when feasible.
- Bound everything: every loop, retry, queue, timeout, scope, and unbounded-source
Flow carries a fixed cap. No unbounded recursion in library code — use tailrec or iterate.
- Exhaustive
when over sealed hierarchies: no else for closed sets, so the compiler flags every new variant.
- Compose via delegation (
by), not inheritance: class delegation for decoration, property delegation for backing-field discipline.
- Zero technical debt: what exists meets the design goals; harden public API fast with
internal and @RequiresOptIn.
Language hard rules
!! is banned outside main/test scaffolding and justified bridges. Resolve nullability at the boundary; internals take non-null types. Use Elvis with a real default or error(...), requireNotNull/checkNotNull for caller-fault contracts.
- No bare
as; prefer as? plus handling, and any cast carries a why-comment.
- Model absence and results as sealed hierarchies;
when over a sealed type stays exhaustive — no else catch-all that defeats narrowing.
- Errors: sealed
Result<T, E> (or kotlin.Result) for expected failures; exceptions only for unrecoverable or programmer-error cases; never swallow.
by delegation for reuse without "is-a"; data class for state, value class for IDs; no inheritance for code reuse; no open class without a documented contract.
- Structured concurrency: coroutines scoped (no
GlobalScope), dispatchers explicit, Flow cold, Channel/buffers bounded, withTimeout on I/O.
- Assert ≥2 per function on average; 60-line hard cap (ktlint/detekt-enforced).
- ktlint + detekt are law; 120-column lines; trailing commas; expression bodies where they read better.
Before you finish — verify
ktlint --format
ktlint
detekt
./gradlew detekt test
Full guide
Deep review
For a full audit (not a quick edit), read reference/checklist.md in this skill and walk every chapter.