Skip to content
Back to skills

Kotlin Conventions

ASecurity

Use when a ticket adds or changes Kotlin code — Android, Ktor/Spring services, multiplatform modules — and it must follow the repo's Kotlin conventions — the Kotlin coding conventions, null-safety without !!, immutability by default, coroutines and Flow used with structured concurrency and correct cancellation, sealed hierarchies for state, detekt/ktlint clean — as the language pack for any Kotlin change. Invoke for "write this in Kotlin", "fix the coroutine leak", "remove the !!", or when re...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 28, 2026
ai-agentsgojavakotlinreactexpressspringapidatabasesecurity

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned September 29, 2026

npx -y skills add tmj-90/gaffer --skill kotlin-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kotlin Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kotlin Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tmj-90-kotlin-conventions/badge)](https://www.skillsdirectory.com/skills/tmj-90-kotlin-conventions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kotlin-conventions
description: Use when a ticket adds or changes Kotlin code — Android, Ktor/Spring services, multiplatform modules — and it must follow the repo's Kotlin conventions — the Kotlin coding conventions, null-safety without !!, immutability by default, coroutines and Flow used with structured concurrency and correct cancellation, sealed hierarchies for state, detekt/ktlint clean — as the language pack for any Kotlin change. Invoke for "write this in Kotlin", "fix the coroutine leak", "remove the !!", or when reviewing a Kotlin diff.
stack: [kotlin, android, jvm]
area: language
---

# Write idiomatic Kotlin

Kotlin's value is safety made concise: nullability in the type, immutability by default,
structured concurrency that cannot leak. For the builder and the reviewer of a Kotlin diff; the module's config and existing code win over it.

## Procedure

1. **Discover the module's conventions first.** Call `search_lore` for Kotlin conventions.
   Read `build.gradle.kts`, `gradle/libs.versions.toml` (Kotlin, coroutines, AGP versions),
   `jvmToolchain`, whether `explicitApi()` is on, `.editorconfig` (ktlint reads
   `ktlint_code_style` and rule toggles there), `detekt.yml` and its baseline, and Android
   `lint.xml`. Identify Android vs server vs multiplatform, the DI framework
   (Hilt/Koin/Spring), how dispatchers are injected, and the state pattern (MVVM/MVI with
   `StateFlow`, or a service layer). Copy a sibling class and its test.
2. **Pin the exact commands** from CI (the `run-tests` and `run-lint` skills). Use the
   defaults below only when the repo defines none; never add to a detekt baseline to pass.
3. **Write the change with the idioms below**, then walk the concurrency section for every
   coroutine, flow, shared value and resource you touched.
4. **Test each acceptance criterion's own behaviour.** One test per AC that fails without
   your change, plus its error path, using `runTest` with the test dispatcher and Turbine
   for flows. If the AC involves shared state or persistence, add a test that launches N
   concurrent coroutines on a multi-threaded dispatcher (`Dispatchers.Default`) and
   asserts the invariant.
5. **Verify, then stop.** Done when: ktlint and detekt are clean at the repo's config, the
   compiler reports no new warnings where `allWarningsAsErrors` is set, tests are green,
   and every AC has a test. Record the output with the `record-evidence` skill; the runner
   submits the work.

## Commands

- All checks: `./gradlew check` (runs tests plus wired-in linters).
- Style: `./gradlew ktlintCheck` (ktlint Gradle plugin) or `ktlint "src/**/*.kt"`;
  `./gradlew detekt`.
- Tests: `./gradlew test`; one class: `./gradlew test --tests 'pkg.ClassTest'`.
- Android: `./gradlew lint testDebugUnitTest` (or the repo's variant).
- Dependencies: a new or bumped entry in `libs.versions.toml` or the build file is a
  blocker (the `dependency-upgrade` skill).

## Idioms that matter

- **Types**: `data class` for values, `sealed interface` for closed state and results,
  `enum class` for fixed sets, `@JvmInline value class` for ids and units; exhaustive
  `when` over sealed types with no `else`, so a new case fails to compile.
- **Null-safety**: non-null by default; `?.`, `?:`, `requireNotNull(x) { "why" }` at
  boundaries; no `!!` on reachable paths; `lateinit` only for framework-injected fields.
  Java calls return platform types (`String!`): give them an explicit nullable or
  non-null type at the boundary.
- **Immutability**: `val` and read-only `List`/`Map` in APIs; build with
  `buildList { }` locally; update data classes with `copy()`. A `data class` with `var`
  properties used as a map key or set element breaks lookups when mutated.
- **Preconditions** with `require` (arguments), `check` (state), `error(...)` (unreachable).
- **Resources**: `use { }` on every `Closeable`.
- **Coding conventions** (kotlinlang.org): expression bodies for one-liners, named
  arguments for boolean and same-typed parameters, scope functions (`let`/`apply`/`also`)
  only where they read better than a local.

## Concurrency and resource safety

- **Structured concurrency**: every coroutine runs in a scope that ends
  (`viewModelScope`, `lifecycleScope`, a service scope with a `SupervisorJob` cancelled on
  shutdown). No `GlobalScope`; no `runBlocking` outside `main` and tests.
- **Cancellation is an exception — do not eat it.** `catch (e: Exception)` and
  `runCatching { suspendCall() }` also catch `CancellationException`, so the coroutine
  keeps running after it was cancelled. Rethrow it (or catch narrower types); in loops call
  `ensureActive()`; cleanup that must suspend goes in `withContext(NonCancellable)`.
- **Dispatchers are injected**; blocking I/O uses `withContext(ioDispatcher)`; never block
  `Dispatchers.Main` or `Default`.
- **`async` without `await`** loses its exception until someone awaits; use `launch` for
  fire-and-forget within a scope, and `coroutineScope { }` to fan out and fail together.
- **Shared mutable state**: `MutableStateFlow.value = value + 1` is a lost-update race; use
  `update { it + 1 }`. Guard other state with `kotlinx.coroutines.sync.Mutex` (a
  `synchronized` block cannot contain a suspension point) and hold it across the whole
  read-modify-write; bound fan-out with `Semaphore`.
- **Flows**: `flowOn` for upstream context, `catch` for upstream errors, `stateIn`/`shareIn`
  in a scope that ends; UI collects with `collectAsStateWithLifecycle` or
  `repeatOnLifecycle`; one-shot events via `Channel` or `SharedFlow(replay = 0)`.
- **Files other requests read** (server/JVM): write to
  `Files.createTempFile(targetDir, …)` then `Files.move(…, ATOMIC_MOVE, REPLACE_EXISTING)`;
  never a fixed temp name. Cross-process exclusion uses a file lock or the database; a
  time-only lease lets a paused holder write late, so writes must check a fencing token or
  version.

## Review checklist — flag as defects

Walk this against the diff. An item is grounds for CHANGES only when, in changed code, it
causes a concrete failure (wrong result, crash, lost or corrupted data, security hole) or
leaves an AC's own behaviour untested: cite the line and that failure. Otherwise it is an
`(optional)` note. Formatting the tools would fix, and preferences the repo
does not enforce, are not findings. Do not patch the code under review.

- [ ] `!!` on a reachable path; an unexplained `lateinit`; a Java platform type flowing
      into Kotlin unannotated.
- [ ] `else` on a `when` over a sealed type the module owns.
- [ ] `GlobalScope`, a scope never cancelled, or `runBlocking` in production code.
- [ ] `CancellationException` swallowed by `catch (e: Exception)` or `runCatching` around
      suspend calls.
- [ ] Blocking I/O on Main/Default; a hard-coded dispatcher where the module injects them.
- [ ] A read-modify-write on shared state (`StateFlow.value`, a map, a counter) without
      `update`, a `Mutex` or an atomic; a check-then-act across a suspension point.
- [ ] A `Closeable` not in `use { }`; a flow collected outside a lifecycle-aware scope.
- [ ] A shared file written in place or via a fixed temp name; a time-only lock lease.
- [ ] Mutable collections or `var`s exposed from a public API.
- [ ] A new detekt/ktlint suppression or baseline entry without a reason.
- [ ] An AC has no test, or a coroutine test uses real delays or `Thread.sleep` instead
      of the test dispatcher.

## Capture lore

This skill is one of the places durable, reusable knowledge naturally surfaces:
**While matching the module you learn the DI framework, how dispatchers are injected and which state-holder pattern the UI uses.** That kind of fact is *lore*. Capture it via the **lore-capture
protocol in your brief** (`CLAUDE.factory.md`, step 11 "Memory contribution"):
call the Memory MCP `suggest_lore` once at the close of your work — reusable
conventions, gotchas, decisions, and boundaries only, never per-ticket trivia.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…