先把 Android 问题归到四层之一——构建、安装、运行时、商店——再对症修复。
编程
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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| target_platform | auto | android | server | multiplatform | library | auto | Picks 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_floor | text (e.g. "2.0", "1.9") | 2.0 | Gates every version-dependent recommendation in build.md; below 2.0 the K2-only and Compose-compiler-plugin advice is suppressed |
| ui_toolkit | compose | views | none | compose | Switches Android examples between compose.md state handling and View/ViewBinding lifecycle patterns in android.md |
| serialization_lib | kotlinx | moshi | gson | jackson | kotlinx | Selects the annotation set, config defaults, and null-safety warnings in serialization.md |
| annotation_processor | ksp | kapt | none | ksp | Drives library setup snippets and the build-speed advice in build.md |
| explicit_api | bool | false | When 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
| Situation | Go to |
|---|---|
NPE on a non-null type, platform type from Java, lateinit not initialized, smart cast refused | null-safety.md |
Coroutine leaks, never cancels, blocks the main thread, wrong dispatcher, runBlocking in production | coroutines.md |
StateFlow stops emitting, SharedFlow drops events, stateIn/shareIn choice, backpressure, cold vs hot | flows.md |
Exception vanishes, async failure surfaces late, runCatching breaks cancellation, error type design | errors.md |
| Wrong list/map/sequence choice, O(n²) lookup, mutation leaking through a read-only type | collections.md |
Scope functions, sealed hierarchies, delegation, destructuring, use, DSL builders | idioms.md |
Variance/out/in compile error, type erasure, reified, inline/crossinline, contracts | generics.md |
Calling Kotlin from Java, @Jvm* annotations, SAM, checked exceptions, converting a Java file | interop.md |
| Lifecycle, ViewModel, SavedStateHandle, process death, leaked Context, flow collection on Android | android.md |
Recomposition storms, remember vs rememberSaveable, stability, side effects, lazy lists | compose.md |
| Coroutine test hangs, passes alone but fails in a suite, virtual time, flow assertions, mocking final classes | testing.md |
| Allocation in a hot path, boxing, sequences vs lists, inlining cost, value classes, benchmarking | performance.md |
| kapt vs KSP, JVM-target mismatch, opt-in/experimental, K2, slow incremental builds, library API rules | build.md |
| JSON null in a non-null property, defaults dropped, polymorphic types, unknown keys, Parcelize | serialization.md |
expect/actual, source sets, Swift/ObjC interop, suspend and Flow across the iOS boundary | multiplatform.md |
| Spring/Ktor, JPA entity as data class, blocking JDBC in a coroutine, MDC/ThreadLocal lost | server.md |
| Anything else | The 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 (android → android.md, server → server.md, multiplatform → multiplatform.md, library → build.md) |
Core Rules
- 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. !!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" }(throwsIllegalArgumentException) orcheckNotNull(IllegalStateException). Each of those produces a message;!!produces a line number and nothing else.- Every coroutine belongs to a scope whose lifetime is ≥ the work's.
GlobalScope, and aCoroutineScope(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). - Cancellation is cooperative. A cancelled coroutine keeps burning CPU until it reaches a suspension point:
while (true) { transform(chunk) }with nosuspendcall inside never stops. AddensureActive()(oryield()) per iteration. Cleanup that suspends after cancellation must run insidewithContext(NonCancellable), or it is cancelled before it does anything. - Never swallow
CancellationException.catch (e: Exception),catch (t: Throwable)andrunCatchingall 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). - 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) }. - 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, propertykotlinx.coroutines.io.parallelism). UI →Dispatchers.Main. For a bounded resource, size the slice to the resource: a 10-connection pool wantsDispatchers.IO.limitedParallelism(10), not 64 threads queueing for 10 connections. equals,hashCodeandcopysee 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 aHashSet, andcopy()silently resets it to its initializer.copy()is also shallow — the copy shares every mutable object the original held.- Make
whenan expression. Assigned or returned, awhenover 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 anelsebranch over a sealed hierarchy silently absorbs every case you add later.
Nullability Decision Table
| The value | Use | Why not the others |
|---|---|---|
| May genuinely be absent in the domain | T? with ?: / ?.let | Absence is data, not an error state |
| Set once before first use, non-primitive, no sensible default | lateinit var | Nullable would push ?. into every call site; lateinit fails loudly with "property X has not been initialized" |
| Expensive, computed on first read | by lazy | Thread-safe by default (SYNCHRONIZED), and no initialization-order trap |
| Required by contract, may be violated by a caller | requireNotNull / checkNotNull | Produces a message and the right exception type at the boundary |
| Primitive type, or null is an acceptable value | nullable or a default value | lateinit 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
| Need | Primitive | Trap it avoids |
|---|---|---|
| Work tied to a lifecycle | scope.launch | Detached work that outlives its owner |
| Two independent results, both required | async + awaitAll | Sequential awaits that double latency |
| Move blocking work off the caller's thread | withContext(Dispatchers.IO) | Blocking a UI or request thread |
| Fan-out where one failure must not kill the rest | supervisorScope | One failed child cancelling its siblings |
| A stream of values over time | Flow (cold) | Manual callback registration and its leaks |
| Current state, always has a value | StateFlow | A late subscriber with nothing to render |
| One-shot events no subscriber may miss | Channel | Navigation/toast events dropped during a configuration change (→ flows.md) |
| Mutual exclusion inside suspend code | Mutex.withLock | synchronized pins a thread while a coroutine holds the lock across a suspension |
| Bounded producer/consumer pipeline | Channel(capacity) | Unbounded buffering that becomes a memory leak |
Equality, Copy, And Identity
==callsequals(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.NaNis false and0.0 == -0.0is true. Once the values go through a boxed or generic path (listOf(Double.NaN).contains(Double.NaN),sortedBy,distinct), Kotlin switches to total order:NaNequals itself and-0.0 < 0.0. Same values, two answers, chosen by the static type. hashCodemust followequals: a hand-writtenequalswith nohashCodeproduces objects that are equal to each other and never found in aHashMap.- A
data classholding anArraycompares by reference, becauseArray.equalsis identity. UseList, or writeequals/hashCodewithcontentEquals/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
| Trap | Why it fails | Do instead |
|---|---|---|
x?.let { … } ?: fallback | The Elvis also fires when the lambda returns null — the fallback runs even though x was non-null | if (x != null) { … } else { … }, or keep the lambda's result non-null |
runBlocking to call a suspend function | Blocks the calling thread; on a UI or request thread it is a freeze, and inside a coroutine it can deadlock the dispatcher | Make the caller suspend, or use the scope that owns the work; runBlocking belongs in main and in tests |
GlobalScope.launch for convenience | Nothing cancels it, and everything it captured lives until process death | A scope owned by the lifecycle of the work (→ coroutines.md) |
Collecting a flow in onCreate or a bare scope | Collection keeps running while the screen is invisible and can touch a destroyed view | repeatOnLifecycle(STARTED) or collectAsStateWithLifecycle (→ android.md) |
@Entity data class (JPA) | Generated equals/hashCode break against lazy proxies and against an id assigned at persist time | Regular class with id-based equals (→ server.md) |
| Reflection-based JSON into non-null properties | The instance is built without calling the constructor, so defaults are skipped and non-null fields hold null | kotlinx.serialization, or Moshi with codegen (→ serialization.md) |
it in nested lambdas | The inner it shadows the outer one; it compiles and does the wrong thing | Name the parameter the moment lambdas nest |
companion object for constants | Every val becomes a getter call, and Java callers must go through Companion | const val for primitives and strings, top-level or in the companion |
Mutable var in a class used as a Map key | hashCode changes after insertion and the entry becomes unreachable | val properties for anything used as a key or put in a Set |
Custom getter or open var used after a null check | Smart 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.collect | Breaks exception transparency and hides which stage failed | The 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)orrunCatchingaround suspend code without re-throwingCancellationException? - Every blocking call inside
withContexton a dispatcher sized for the resource, and never onMain? - Every
whenover a sealed type used as an expression, with noelsecovering future subclasses? - Library module: explicit visibility and return types if
explicit_apiis 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/catcharound it, it was never exceptional. The contested part iskotlin.Resultas 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:
Channelvs state. AChanneldelivers 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/runchains 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.
Related Skills
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 boundaryandroid— Android build system, release configuration, and deploymentandroid-studio— IDE workflow: debugging, profiling, refactoringswift— the Swift side when Kotlin Multiplatform ships an iOS framework
Feedback
- If useful, star it: https://clawic.com/skills/kotlin
- Latest version: https://clawic.com/skills/kotlin
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)。
相关技能
按配置的 JDK 版本诊断 Java 与 JVM 问题(从 NPE 到容器 OOM),给出可直接套用的代码与配置。
把 Flutter 框架异常读成具体修复动作,再把 release 包真正跑到设备上。
针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。
为 Rust 编译错误、运行时故障、async 陷阱与 Cargo 构建问题提供结构化排查清单。
调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。