编程

Kotlin

为 Kotlin 代码提供正确性与惯用法诊断:空安全、协程、Flow、Compose 状态、Java 互操作与构建问题。

它能做什么

针对 Kotlin 运行时与构建中的具体问题给出修复方案:Java 平台类型引发的 NPE、协程泄漏或吞掉 CancellationException、StateFlow 因 conflation 停止更新、Compose 重组风暴、kapt/KSP 与 JVM target 不匹配。依据空安全、结构化并发、密封类穷尽性与相等性规则审查生成的 Kotlin。覆盖 Android、Spring/Ktor 服务端以及 Kotlin Multiplatform 场景。

什么时候用它

  • 非空类型上抛 NPE,或来自 Java 的平台类型
  • 协程泄漏、吞掉取消异常,测试卡住
  • StateFlow 不再发射,或 Compose 过度重组
  • Java→Kotlin 迁移,以及 Kotlin Multiplatform 跨端配置

技能文档

User preferences and memory live in ~/Clawic/data/kotlin/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/kotlin/ or ~/clawic/kotlin/), move it to ~/Clawic/data/kotlin/.

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/kotlin/config.yaml.

VariableTypeDefaultEffect
target_platformauto | android | server | multiplatform | libraryautoPicks the default deep-dive file and the idiom set; auto infers from build files (AGP plugin → android, Ktor/Spring dependency → server, kotlin("multiplatform") → multiplatform)
kotlin_floortext (e.g. "2.0", "1.9")2.0Gates every version-dependent recommendation in build.md; below 2.0 the K2-only and Compose-compiler-plugin advice is suppressed
ui_toolkitcompose | views | nonecomposeSwitches Android examples between compose.md state handling and View/ViewBinding lifecycle patterns in android.md
serialization_libkotlinx | moshi | gson | jacksonkotlinxSelects the annotation set, config defaults, and null-safety warnings in serialization.md
annotation_processorksp | kapt | nonekspDrives library setup snippets and the build-speed advice in build.md
explicit_apiboolfalseWhen true, emitted library code carries explicit visibility and public return types, matching explicitApi() strictness

Preference areas to record as the user reveals them:

  • tooling — DI (Hilt, Koin, manual), networking client (Retrofit, Ktor client), persistence (Room, SQLDelight, Exposed, JPA), test stack (JUnit4/5, MockK, flow-testing library, Kotest) — affects which examples and setup snippets are offered
  • conventions — package layout (feature vs layer), formatter/linter (ktlint, ktfmt, detekt), trailing commas, explicit types on public API, naming of sealed state hierarchies — affects generated code and review verdicts
  • platform — JVM toolchain version, Android min SDK, KMP target list, whether Java sources coexist in the module — affects which floors and interop rules apply
  • safety posture — tolerance for !!, experimental/opt-in APIs, warning suppression, allWarningsAsErrors — affects how hard to push back in review
  • legacy bridges — RxJava, LiveData, callback APIs still in the codebase — affects whether answers stay pure-coroutine or include adapters
  • output format — KDoc density, tests emitted alongside code, snippet vs full file — affects the shape of every deliverable

When To Use

  • Writing or reviewing Kotlin for correctness: nullability, coroutines, collections, sealed hierarchies, equality
  • Debugging Kotlin runtime surprises: NPE on a non-null type, coroutine that never finishes or never cancels, flow that stops emitting, recomposition storm, test that hangs
  • Java interop and Java→Kotlin migration, including making Kotlin APIs pleasant to call from Java
  • Build and compiler problems that are Kotlin's, not Gradle's: kapt/KSP, JVM-target mismatch, opt-in, K2 migration
  • Sharing code across Android, iOS, JVM, or web with Kotlin Multiplatform, or writing server-side Kotlin
  • Not for Java-only codebases (→ java), Android release configuration, signing and distribution (→ android), or IDE workflow (→ android-studio)

Quick Reference

SituationGo to
NPE on a non-null type, platform type from Java, lateinit not initialized, smart cast refusednull-safety.md
Coroutine leaks, never cancels, blocks the main thread, wrong dispatcher, runBlocking in productioncoroutines.md
StateFlow stops emitting, SharedFlow drops events, stateIn/shareIn choice, backpressure, cold vs hotflows.md
Exception vanishes, async failure surfaces late, runCatching breaks cancellation, error type designerrors.md
Wrong list/map/sequence choice, O(n²) lookup, mutation leaking through a read-only typecollections.md
Scope functions, sealed hierarchies, delegation, destructuring, use, DSL buildersidioms.md
Variance/out/in compile error, type erasure, reified, inline/crossinline, contractsgenerics.md
Calling Kotlin from Java, @Jvm* annotations, SAM, checked exceptions, converting a Java fileinterop.md
Lifecycle, ViewModel, SavedStateHandle, process death, leaked Context, flow collection on Androidandroid.md
Recomposition storms, remember vs rememberSaveable, stability, side effects, lazy listscompose.md
Coroutine test hangs, passes alone but fails in a suite, virtual time, flow assertions, mocking final classestesting.md
Allocation in a hot path, boxing, sequences vs lists, inlining cost, value classes, benchmarkingperformance.md
kapt vs KSP, JVM-target mismatch, opt-in/experimental, K2, slow incremental builds, library API rulesbuild.md
JSON null in a non-null property, defaults dropped, polymorphic types, unknown keys, Parcelizeserialization.md
expect/actual, source sets, Swift/ObjC interop, suspend and Flow across the iOS boundarymultiplatform.md
Spring/Ktor, JPA entity as data class, blocking JDBC in a coroutine, MDC/ThreadLocal lostserver.md
Anything elseThe rules and tables below; for pure language semantics, idioms.md then generics.md; with no situational match at all, open the file target_platform resolves to (androidandroid.md, serverserver.md, multiplatformmultiplatform.md, librarybuild.md)

Core Rules

  1. Nullability is a boundary problem. Non-null Kotlin types are only as true as the code that fills them: Java, reflection-based JSON, and framework callbacks can all put null into a String. Validate at the boundary — parse into a nullable DTO, then map into a non-null domain type. An NPE thrown at the boundary names the field; one thrown three layers later names nothing.
  2. !! requires an alternative to have been rejected. Decision order: value arrives later on one thread → lateinit var; computed once on first use → by lazy; legitimately absent → nullable + ?:; caller broke a contract → requireNotNull(x) { "id missing" } (throws IllegalArgumentException) or checkNotNull (IllegalStateException). Each of those produces a message; !! produces a line number and nothing else.
  3. Every coroutine belongs to a scope whose lifetime is ≥ the work's. GlobalScope, and a CoroutineScope(Dispatchers.IO) stored in a field with no cancellation, are leaks by construction: an abandoned screen keeps its network call, its callbacks, and everything they captured. Structured concurrency is the guarantee that "the caller returned" means "the work is over" (→ coroutines.md).
  4. Cancellation is cooperative. A cancelled coroutine keeps burning CPU until it reaches a suspension point: while (true) { transform(chunk) } with no suspend call inside never stops. Add ensureActive() (or yield()) per iteration. Cleanup that suspends after cancellation must run inside withContext(NonCancellable), or it is cancelled before it does anything.
  5. Never swallow CancellationException. catch (e: Exception), catch (t: Throwable) and runCatching all capture it, converting "the parent cancelled me" into "I failed" — and the parent then waits on a child that reported success. Shape: catch (e: CancellationException) { throw e } catch (e: IOException) { … } — cancellation first, specific types after (→ errors.md).
  6. StateFlow conflates by equals. Emitting a value equal to the current one runs no collector. Mutate a list in place and re-assign the same reference and nothing is emitted (the new value equals the old one), so the UI "stops updating" with no error anywhere. Publish a new immutable value: _state.update { it.copy(items = it.items + new) }.
  7. Choose the dispatcher by what the code blocks on. CPU work → Dispatchers.Default (parallelism = CPU cores, minimum 2). Blocking calls, JDBC, file I/O, legacy SDKs → Dispatchers.IO (64 threads by default, property kotlinx.coroutines.io.parallelism). UI → Dispatchers.Main. For a bounded resource, size the slice to the resource: a 10-connection pool wants Dispatchers.IO.limitedParallelism(10), not 64 threads queueing for 10 connections.
  8. equals, hashCode and copy see only the constructor properties. A property declared in the class body is invisible to all three: two objects differing only there are equal, collide in a HashSet, and copy() silently resets it to its initializer. copy() is also shallow — the copy shares every mutable object the original held.
  9. Make when an expression. Assigned or returned, a when over a sealed type is checked for exhaustiveness, so adding a subclass breaks the build at every decision point that must change. As a bare statement it has only been an error since Kotlin >=1.7, and an else branch over a sealed hierarchy silently absorbs every case you add later.

Nullability Decision Table

The valueUseWhy not the others
May genuinely be absent in the domainT? with ?: / ?.letAbsence is data, not an error state
Set once before first use, non-primitive, no sensible defaultlateinit varNullable would push ?. into every call site; lateinit fails loudly with "property X has not been initialized"
Expensive, computed on first readby lazyThread-safe by default (SYNCHRONIZED), and no initialization-order trap
Required by contract, may be violated by a callerrequireNotNull / checkNotNullProduces a message and the right exception type at the boundary
Primitive type, or null is an acceptable valuenullable or a default valuelateinit supports neither primitives nor nullable types

Generic caveat: an unbounded T includes T?, so fun firstOr(x: T): T happily accepts null. Write `` when the parameter must be non-null.

Concurrency Primitive Selection

NeedPrimitiveTrap it avoids
Work tied to a lifecyclescope.launchDetached work that outlives its owner
Two independent results, both requiredasync + awaitAllSequential awaits that double latency
Move blocking work off the caller's threadwithContext(Dispatchers.IO)Blocking a UI or request thread
Fan-out where one failure must not kill the restsupervisorScopeOne failed child cancelling its siblings
A stream of values over timeFlow (cold)Manual callback registration and its leaks
Current state, always has a valueStateFlowA late subscriber with nothing to render
One-shot events no subscriber may missChannelNavigation/toast events dropped during a configuration change (→ flows.md)
Mutual exclusion inside suspend codeMutex.withLocksynchronized pins a thread while a coroutine holds the lock across a suspension
Bounded producer/consumer pipelineChannel(capacity)Unbounded buffering that becomes a memory leak

Equality, Copy, And Identity

  • == calls equals (structural); === compares references. This is the opposite convention to Java — a Java-trained reviewer reads == as identity and approves a real bug.
  • Floating point has two behaviours. With a static type of Double/Float, == is IEEE 754: Double.NaN == Double.NaN is false and 0.0 == -0.0 is true. Once the values go through a boxed or generic path (listOf(Double.NaN).contains(Double.NaN), sortedBy, distinct), Kotlin switches to total order: NaN equals itself and -0.0 < 0.0. Same values, two answers, chosen by the static type.
  • hashCode must follow equals: a hand-written equals with no hashCode produces objects that are equal to each other and never found in a HashMap.
  • A data class holding an Array compares by reference, because Array.equals is identity. Use List, or write equals/hashCode with contentEquals/contentHashCode.
  • Value classes (@JvmInline value class) are the underlying type at runtime until they are used as a generic argument, made nullable, or passed through an implemented interface — then they box (→ performance.md).

Traps

TrapWhy it failsDo instead
x?.let { … } ?: fallbackThe Elvis also fires when the lambda returns null — the fallback runs even though x was non-nullif (x != null) { … } else { … }, or keep the lambda's result non-null
runBlocking to call a suspend functionBlocks the calling thread; on a UI or request thread it is a freeze, and inside a coroutine it can deadlock the dispatcherMake the caller suspend, or use the scope that owns the work; runBlocking belongs in main and in tests
GlobalScope.launch for convenienceNothing cancels it, and everything it captured lives until process deathA scope owned by the lifecycle of the work (→ coroutines.md)
Collecting a flow in onCreate or a bare scopeCollection keeps running while the screen is invisible and can touch a destroyed viewrepeatOnLifecycle(STARTED) or collectAsStateWithLifecycle (→ android.md)
@Entity data class (JPA)Generated equals/hashCode break against lazy proxies and against an id assigned at persist timeRegular class with id-based equals (→ server.md)
Reflection-based JSON into non-null propertiesThe instance is built without calling the constructor, so defaults are skipped and non-null fields hold nullkotlinx.serialization, or Moshi with codegen (→ serialization.md)
it in nested lambdasThe inner it shadows the outer one; it compiles and does the wrong thingName the parameter the moment lambdas nest
companion object for constantsEvery val becomes a getter call, and Java callers must go through Companionconst val for primitives and strings, top-level or in the companion
Mutable var in a class used as a Map keyhashCode changes after insertion and the entry becomes unreachableval properties for anything used as a key or put in a Set
Custom getter or open var used after a null checkSmart cast is refused because the value can change between reads; the reflex fix is !!Assign to a local val and smart-cast that
try/catch wrapped around flow.collectBreaks exception transparency and hides which stage failedThe catch operator upstream of collect (→ errors.md)

Output Gates

Before emitting Kotlin code or a review verdict, verify:

  • No !! that rule 2's decision order could have replaced?
  • Every coroutine started in a scope something cancels, and every long CPU loop cooperating with cancellation?
  • No catch (Exception) or runCatching around suspend code without re-throwing CancellationException?
  • Every blocking call inside withContext on a dispatcher sized for the resource, and never on Main?
  • Every when over a sealed type used as an expression, with no else covering future subclasses?
  • Library module: explicit visibility and return types if explicit_api is on, plus @Jvm* annotations wherever Java calls in?
  • State published as a new immutable value, not a mutated collection re-emitted through the same reference?

Where Experts Disagree

  • Typed failures vs exceptions. Return a typed failure for expected outcomes (validation, not-found, offline); throw for broken invariants and unrecoverable I/O. The test that settles most arguments: if every caller writes try/catch around it, it was never exceptional. The contested part is kotlin.Result as a public return type — it boxes, it cannot be matched exhaustively, and it hides which failures exist, while a sealed hierarchy enumerates them (→ errors.md).
  • One-shot events: Channel vs state. A Channel delivers exactly once to one consumer and survives a gap between subscribers; modelling the event as a field of the UI state with an explicit "consumed" acknowledgement survives process death but costs a round trip. Both are defensible. SharedFlow(replay = 0) is the option that quietly drops events while nobody is collecting.
  • Mocks vs fakes. Kotlin classes are final by default, so mocking frameworks need bytecode tricks that make brittle tests brittler; a hand-written fake implementing the interface survives refactors. Mocks earn their keep for verifying interactions with something you do not own (→ testing.md).
  • Scope-function density. One camp reads apply/also/run chains as idiomatic Kotlin, the other as write-only code. The neutral line: a scope function that removes a temporary variable is a win; nesting beyond one level is a named function waiting to be extracted.

More Clawic skills, get them at https://clawic.com/skills/kotlin (install if the user confirms):

  • java — the language on the other side of every interop boundary
  • android — Android build system, release configuration, and deployment
  • android-studio — IDE workflow: debugging, profiling, refactoring
  • swift — the Swift side when Kotlin Multiplatform ships an iOS framework

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/kotlin.

常见问题

它能处理纯 Java 代码库吗?
不能。范围限定在 Kotlin,纯 Java 工作不在覆盖之内,需交由独立的 Java 技能处理。
能在 Java/Kotlin 混合模块中使用吗?
可以。涵盖 Java 互操作、@Jvm* 注解、SAM 转换与受检异常的处理。
它会配置 Android 发布构建或签名吗?
不会。该部分交由独立的 Android 技能负责;本技能只覆盖 Kotlin 侧的构建问题(kapt/KSP、JVM target、opt-in、K2)。

相关技能

先把 Android 问题归到四层之一——构建、安装、运行时、商店——再对症修复。

115 次安装4 星标

按配置的 JDK 版本诊断 Java 与 JVM 问题(从 NPE 到容器 OOM),给出可直接套用的代码与配置。

作者 Iván130 次安装9 星标

把 Flutter 框架异常读成具体修复动作,再把 release 包真正跑到设备上。

119 次安装6 星标

针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。

121 次安装8 星标

为 Rust 编译错误、运行时故障、async 陷阱与 Cargo 构建问题提供结构化排查清单。

109 次安装4 星标

调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。

82 次安装3 星标