Install
openclaw skills install @ivangdavila/kotlinWrites, debugs, and reviews Kotlin: coroutines and flows, null safety, collections, Java interop, and Compose state. Use when an NPE hits a non-null type, !! or a Java platform type blows up, a coroutine leaks, never cancels, or swallows exceptions, a StateFlow stops emitting or a SharedFlow drops events, a Compose screen recomposes too much or loses state on rotation, equals/copy/== behave unexpectedly, a when stops being exhaustive, kapt/KSP or JVM-target errors break the Gradle build, JSON puts null into a non-null property, a coroutine test hangs or passes only in isolation; also when porting Java to Kotlin, sharing code across Android/iOS/JVM, tuning allocation and inlining, or writing server-side Kotlin with Spring or Ktor. Not for Java-only codebases or Android release/build-system configuration.
openclaw skills install @ivangdavila/kotlinUser 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/.
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:
!!, experimental/opt-in APIs, warning suppression, allWarningsAsErrors — affects how hard to push back in reviewjava), Android release configuration, signing and distribution (→ android), or IDE workflow (→ android-studio)| 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) |
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" } (throws IllegalArgumentException) or checkNotNull (IllegalStateException). Each of those produces a message; !! produces a line number and nothing else.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).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.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).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) }.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.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.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.| 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 <T> firstOr(x: T): T happily accepts null. Write <T : Any> when the parameter must be non-null.
| 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 |
== calls equals (structural); === compares references. This is the opposite convention to Java — a Java-trained reviewer reads == as identity and approves a real bug.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.data class holding an Array compares by reference, because Array.equals is identity. Use List, or write equals/hashCode with contentEquals/contentHashCode.@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).| 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) |
Before emitting Kotlin code or a review verdict, verify:
!! that rule 2's decision order could have replaced?catch (Exception) or runCatching around suspend code without re-throwing CancellationException?withContext on a dispatcher sized for the resource, and never on Main?when over a sealed type used as an expression, with no else covering future subclasses?explicit_api is on, plus @Jvm* annotations wherever Java calls in?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).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.testing.md).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 boundaryandroid — Android build system, release configuration, and deploymentandroid-studio — IDE workflow: debugging, profiling, refactoringswift — the Swift side when Kotlin Multiplatform ships an iOS frameworkPart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/kotlin.