$npx -y skills add margelo/react-native-skills --skill kotlinDesign, implement, and review Kotlin and Android APIs. Use when working on .kt files, Kotlin nullability, sealed classes/interfaces, coroutines, Android threading, Java interop, or Kotlin-backed React Native Nitro Module implementations.
| 1 | # Kotlin |
| 2 | |
| 3 | Use this skill for Kotlin code that needs strong type modeling, explicit async boundaries, and idiomatic Android integration. When the code is part of a Nitro Module, pair this with `build-nitro-modules` for generated specs, Promise mapping, annotations, and HybridObject constraints. |
| 4 | |
| 5 | ## Workflow |
| 6 | |
| 7 | 1. Read the local Kotlin code, generated abstract classes, and public API shape before editing. |
| 8 | 2. Model valid states with Kotlin types before implementing behavior. |
| 9 | 3. Choose whether work is synchronous, coroutine-based, or explicitly dispatched. |
| 10 | 4. Keep properties cheap and nonblocking. |
| 11 | 5. Make thread hops, Android service access, I/O, and fallible work visible in the API. |
| 12 | |
| 13 | ## Type-Safe API Design |
| 14 | |
| 15 | - Represent state variants with sealed interfaces/classes, normal interfaces, or concrete classes. Do not encode variants as one data class with many nullable fields. |
| 16 | - Keep related values non-null on the variant where they are valid. If `barcode` and `barcodeType` must exist together, put them on `ScannedBarcode`. |
| 17 | - Use nullable types for real absence inside one state, not for "maybe this object is a different kind of thing". |
| 18 | - Prefer compile-time narrowing over caller-side null probing. Repeated null checks for related fields usually mean the model is too loose. |
| 19 | |
| 20 | ```kotlin |
| 21 | // Avoid: related fields are nullable and the valid combinations are implicit. |
| 22 | data class ScannedData( |
| 23 | val position: Point, |
| 24 | val text: String?, |
| 25 | val barcode: String?, |
| 26 | val barcodeType: BarcodeType?, |
| 27 | val face: Rect?, |
| 28 | ) |
| 29 | |
| 30 | // Prefer: each variant exposes only the fields that are valid for it. |
| 31 | sealed interface ScannedData { |
| 32 | val position: Point |
| 33 | } |
| 34 | |
| 35 | data class ScannedText( |
| 36 | override val position: Point, |
| 37 | val text: String, |
| 38 | ) : ScannedData |
| 39 | |
| 40 | data class ScannedBarcode( |
| 41 | override val position: Point, |
| 42 | val barcode: String, |
| 43 | val barcodeType: BarcodeType, |
| 44 | ) : ScannedData |
| 45 | |
| 46 | data class ScannedFace( |
| 47 | override val position: Point, |
| 48 | val face: Rect, |
| 49 | ) : ScannedData |
| 50 | ``` |
| 51 | |
| 52 | ## Async and Threading |
| 53 | |
| 54 | - Use coroutines for naturally suspending APIs and Android I/O that already has coroutine support. |
| 55 | - Keep quick, deterministic, local work synchronous. Do not introduce coroutines, dispatchers, or Promise plumbing for simple value construction, cached metadata, or pure transforms. |
| 56 | - Use explicit owned dispatchers/scopes or Nitro `Promise.parallel` for CPU-bound synchronous work that should not run on the caller thread. |
| 57 | - Avoid the main dispatcher unless the Android API requires it, such as view/UI mutation or lifecycle APIs. Keep main-thread blocks small and move parsing, conversion, I/O, session negotiation, and CPU work to an owned dispatcher or async API. |
| 58 | - Do not use `runBlocking` in library code, property getters, setters, or JS-facing entry points. |
| 59 | - Do not hide a thread hop, service lookup, blocking I/O, permission flow, or fallible native operation behind a property. |
| 60 | - If a value is emitted by a specific Android callback/thread, prefer a listener, Flow, or event API. In Nitro specs, expose a listener or a `Promise<T>` method. |
| 61 | - Treat repeated `launch`, `withContext`, dispatcher, handler, or executor hops as an architecture smell. A component should either own the coroutine scope/dispatcher/lifecycle it works on, or cross into that owner once at the public async boundary or native callback boundary. |
| 62 | - If a workflow bounces between main, IO, default, native, and JS/Nitro contexts in multiple nested places, stop and redesign the object/lifecycle/API. Excessive hops hide latency, make ordering harder to reason about, and create future performance problems. |
| 63 | - Do not fix races or readiness bugs with `delay`, `Thread.sleep`, `Handler.postDelayed`, timers, extra dispatcher hops, or calling the same method twice. Fix the owner dispatcher/lifecycle, state machine, callback/event, Flow, or async API boundary instead. Retry only for external nondeterminism such as hardware, OS services, remote services, or network, and keep retries bounded, cancellable, and idempotent. |
| 64 | - Keep mutable shared state owned by one coroutine scope, dispatcher, lock, or Android component lifecycle. Avoid mixing ownership models without a clear boundary. |
| 65 | - Treat `Mutex`, `synchronized`, and `ReentrantLock` as last-resort synchronization, not default listener or callback plumbing. Before adding one, identify the exact shared mutable values, concurrent callers, and why a single owner dispatcher/lifecycle, channel/Flow, or immutable snapshot is not enough. |
| 66 | - Never hold a lock while invoking callbacks, calling into JS/Nitro, or calling unknown user code. Snapshot listeners under the lock if needed, unlock, then call them. A listener removed during an in-flight emission may receive that current event. |
| 67 | |
| 68 | ## Properties |
| 69 | |
| 70 | - U |