kotlin

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Kotlin

Kotlin

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.
本技能适用于需要强类型建模、明确异步边界以及符合 Android 惯用集成方式的 Kotlin 代码。如果代码属于 Nitro Module 的一部分,请搭配
build-nitro-modules
使用,以处理生成的规范、Promise 映射、注解以及 HybridObject 约束。

Workflow

工作流程

  1. Read the local Kotlin code, generated abstract classes, and public API shape before editing.
  2. Model valid states with Kotlin types before implementing behavior.
  3. Choose whether work is synchronous, coroutine-based, or explicitly dispatched.
  4. Keep properties cheap and nonblocking.
  5. Make thread hops, Android service access, I/O, and fallible work visible in the API.
  1. 编辑前先阅读本地 Kotlin 代码、生成的抽象类以及公共 API 结构。
  2. 在实现行为之前,使用 Kotlin 类型为有效状态建模。
  3. 选择工作的执行方式:同步、基于 coroutine,还是显式调度。
  4. 确保属性的访问是轻量且非阻塞的。
  5. 在 API 中显式体现线程切换、Android 服务访问、I/O 以及可能出错的操作。

Type-Safe API Design

类型安全的 API 设计

  • Represent state variants with sealed interfaces/classes, normal interfaces, or concrete classes. Do not encode variants as one data class with many nullable fields.
  • Keep related values non-null on the variant where they are valid. If
    barcode
    and
    barcodeType
    must exist together, put them on
    ScannedBarcode
    .
  • Use nullable types for real absence inside one state, not for "maybe this object is a different kind of thing".
  • Prefer compile-time narrowing over caller-side null probing. Repeated null checks for related fields usually mean the model is too loose.
kotlin
// Avoid: related fields are nullable and the valid combinations are implicit.
data class ScannedData(
  val position: Point,
  val text: String?,
  val barcode: String?,
  val barcodeType: BarcodeType?,
  val face: Rect?,
)

// Prefer: each variant exposes only the fields that are valid for it.
sealed interface ScannedData {
  val position: Point
}

data class ScannedText(
  override val position: Point,
  val text: String,
) : ScannedData

data class ScannedBarcode(
  override val position: Point,
  val barcode: String,
  val barcodeType: BarcodeType,
) : ScannedData

data class ScannedFace(
  override val position: Point,
  val face: Rect,
) : ScannedData
  • 使用 sealed interfaces/classes、普通接口或具体类表示状态变体。不要将多种变体编码为一个包含大量可空字段的 data class。
  • 将相关值放在其有效的变体上,保持非空。如果
    barcode
    和
    barcodeType
    必须同时存在,则将它们放在
    ScannedBarcode
    中。
  • 可空类型仅用于表示单个状态内的真实缺失,而非用于“该对象可能是另一种类型”的场景。
  • 优先使用编译时类型收窄,而非调用方的空值探测。对相关字段重复进行空检查通常意味着模型过于松散。
kotlin
// Avoid: related fields are nullable and the valid combinations are implicit.
data class ScannedData(
  val position: Point,
  val text: String?,
  val barcode: String?,
  val barcodeType: BarcodeType?,
  val face: Rect?,
)

// Prefer: each variant exposes only the fields that are valid for it.
sealed interface ScannedData {
  val position: Point
}

data class ScannedText(
  override val position: Point,
  val text: String,
) : ScannedData

data class ScannedBarcode(
  override val position: Point,
  val barcode: String,
  val barcodeType: BarcodeType,
) : ScannedData

data class ScannedFace(
  override val position: Point,
  val face: Rect,
) : ScannedData

Async and Threading

异步与线程处理

  • Use coroutines for naturally suspending APIs and Android I/O that already has coroutine support.
  • Keep quick, deterministic, local work synchronous. Do not introduce coroutines, dispatchers, or Promise plumbing for simple value construction, cached metadata, or pure transforms.
  • Use explicit owned dispatchers/scopes or Nitro
    Promise.parallel
    for CPU-bound synchronous work that should not run on the caller thread.
  • 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.
  • Do not use
    runBlocking
    in library code, property getters, setters, or JS-facing entry points.
  • Do not hide a thread hop, service lookup, blocking I/O, permission flow, or fallible native operation behind a property.
  • 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.
  • 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.
  • 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.
  • 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.
  • Keep mutable shared state owned by one coroutine scope, dispatcher, lock, or Android component lifecycle. Avoid mixing ownership models without a clear boundary.
  • 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.
  • 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.
  • 对于天然可挂起的 API 以及已有 coroutine 支持的 Android I/O 操作,使用 coroutines。
  • 对于快速、确定的本地工作,保持同步执行。不要为简单的值构造、缓存元数据或纯转换操作引入 coroutines、dispatchers 或 Promise 相关的复杂逻辑。
  • 对于不应在调用方线程运行的 CPU 密集型同步工作,使用显式的自有 dispatchers/scopes,或 Nitro 的
    Promise.parallel
    。
  • 避免使用 main dispatcher,除非 Android API 有要求,例如视图/UI 变更或生命周期 API。保持主线程代码块尽可能小,将解析、转换、I/O、会话协商以及 CPU 密集型工作移至自有 dispatcher 或异步 API 中执行。
  • 不要在库代码、属性 getter/setter 或面向 JS 的入口点中使用
    runBlocking
    。
  • 不要将线程切换、服务查找、阻塞 I/O、权限流程或可能出错的原生操作隐藏在属性背后。
  • 如果值由特定的 Android 回调/线程发出,优先使用 listener、Flow 或事件 API。在 Nitro 规范中,暴露 listener 或
    Promise<T>
    方法。
  • 将重复的
    launch
    、
    withContext
    、dispatcher、handler 或 executor 切换视为架构坏味道。一个组件要么拥有其运行所需的 coroutine scope/dispatcher/生命周期,要么在公共异步边界或原生回调边界处一次性进入该所有者的上下文。
  • 如果一个工作流在 main、IO、default、原生以及 JS/Nitro 上下文之间多次嵌套跳转,请停止并重新设计对象/生命周期/API。过多的上下文切换会掩盖延迟,让执行顺序更难推理,并带来未来的性能问题。
  • 不要通过
    delay
    、
    Thread.sleep
    、
    Handler.postDelayed
    、定时器、额外的 dispatcher 切换或重复调用同一方法来修复竞态条件或就绪性问题。应通过修复所有者 dispatcher/生命周期、状态机、回调/事件、Flow 或异步 API 边界来解决。仅针对硬件、操作系统服务、远程服务或网络等外部非确定性场景进行重试,且重试需有次数限制、可取消并保持幂等。
  • 可变共享状态应由单个 coroutine scope、dispatcher、锁或 Android 组件生命周期所有。若无明确边界,避免混合使用所有权模型。
  • 将
    Mutex
    、
    synchronized
    和
    ReentrantLock
    视为最后的同步手段,而非默认的 listener 或回调实现方式。在添加同步机制之前,需明确具体的共享可变值、并发调用方,以及为何单一所有者 dispatcher/生命周期、channel/Flow 或不可变快照无法满足需求。
  • 绝对不要在持有锁的情况下调用回调、调用 JS/Nitro 或执行未知的用户代码。如有需要,在持有锁时对 listener 做快照,释放锁后再调用它们。在事件发送过程中被移除的 listener 可能仍会收到当前事件。

Properties

属性

  • Use
    val
    and
    var
    for cheap in-memory state or derived values.
  • Use methods for side effects, I/O, Android service calls, permission checks, allocation, blocking work, or operations that can fail.
  • If a property would need to wait for another thread, redesign it as an async method or event.
kotlin
// Avoid: blocking work hidden in a getter.
val status: SessionStatus
  get() = runBlocking { session.status() }

// Prefer: make the async boundary explicit.
fun getStatus(): Promise<SessionStatus> {
  return Promise.async {
    session.status()
  }
}
  • 对于轻量的内存状态或派生值,使用
    val
    和
    var
    。
  • 对于副作用、I/O、Android 服务调用、权限检查、内存分配、阻塞工作或可能失败的操作,使用方法。
  • 如果某个属性需要等待另一个线程的结果,应将其重新设计为异步方法或事件。
kotlin
// Avoid: blocking work hidden in a getter.
val status: SessionStatus
  get() = runBlocking { session.status() }

// Prefer: make the async boundary explicit.
fun getStatus(): Promise<SessionStatus> {
  return Promise.async {
    session.status()
  }
}

Kotlin Style

Kotlin 编码风格

  • Make classes final by default. Kotlin classes already are final unless marked
    open
    ; keep them that way unless inheritance is intentional.
  • Prefer
    data class
    for plain values and regular classes for owned resources or lifecycle.
  • Prefer
    sealed interface
    or
    sealed class
    for closed result families.
  • Use
    require
    ,
    check
    , or specific exceptions for invalid inputs and invalid state. Do not silently no-op user-reachable failures.
  • Keep Java interop explicit. Convert platform types into Kotlin types before exposing them through the public API where practical.
  • Avoid
    !!
    except at narrow boundaries where a prior check makes the invariant obvious. Prefer early returns,
    requireNotNull
    , or typed state.
  • Prefer explicit
    return
    statements inside multi-line control flow. Do not lift the return outside a multi-line
    try
    /
    catch
    ,
    if
    ,
    when
    , or lambda just to make it expression-like; use
    try { return value } catch (...) { return fallback }
    .
  • In multi-line lambdas, use labeled returns such as
    return@map value
    for the result. Omit the label only for true single-expression lambdas like
    items.map { it.toString() }
    ; do not write
    items.map { return@map it.toString() }
    .
  • Prefer inline shorthand for unambiguous single-expression lambdas:
    formats.map { it.toMLKitFormat() }
    , not a multi-line
    map { format -> format.toMLKitFormat() }
    . Do not use
    it
    when a surrounding or nested lambda already uses
    it
    ; name parameters in nested lambdas or when clarity needs it.
  • Treat a filename as a scope contract.
    HybridDataScanner.kt
    should implement
    HybridDataScanner
    ; it should not also contain Android helpers, geometry conversions, listeners, or extension utilities.
  • Keep one top-level implementation type per file. Do not put secondary classes, interfaces, enums, option adapters, coordinators, delegates, or helper types below the primary type. If a helper deserves a type, it deserves its own file.
  • Keep one focused extension/conversion per file. Do not create a catch-all extension file just because every helper is an extension. Split conversions into named files such as
    Barcode+toScannedCode.kt
    ,
    TargetBarcodeFormat+toMLKitFormat.kt
    , and
    BarcodeFormat+fromMLKitBarcodeFormat.kt
    with
    internal
    visibility where appropriate.
  • Never put extension
    fun
    ,
    val
    , or
    var
    declarations inside
    Hybrid*
    implementation files or other primary implementation files, even when they are private, tiny, or only used by that file. Put every extension in a separate named
    Type+operation.kt
    extension/converter file so code splitting, maintainability, and future diffs stay clean.
  • Do not place private top-level extension properties/functions below a primary class just because Kotlin allows it. Companion extensions, enum/platform mappings, barcode format conversions, and ML Kit/CameraX adapters belong in their own named files.
  • Use private top-level helpers in the same file only when they are tiny, non-extension functions/properties, and exist solely to support that file's primary type.
  • Extract Android preflight checks and platform boilerplate out of
    Hybrid*
    factories and implementation methods. Permission checks, manifest feature/permission validation,
    PackageManager
    capability checks, service availability checks, and similar setup guards belong in focused helper files; the HybridObject call site should stay one or two lines.
  • Do not create broad
    Utils.kt
    files for these helpers. Name the file after the platform type or domain check and keep each helper focused on one behavior.
  • Use line count as a review signal: files below roughly 300 lines are usually fine only after the one-type-per-file and no-helpers-in-factory rules are satisfied. Size caused by helpers, conversions, or Android glue is a design issue.
  • Put conversions on the element type, not on
    List
    or array types, when the conversion only reads one element. Prefer
    TargetBarcodeFormat.toMLKitFormat()
    plus
    formats.map { it.toMLKitFormat() }
    at the call site over
    Array<TargetBarcodeFormat>.toMLKitFormats()
    .
  • Do not put domain conversions on broad/common receivers such as
    Int
    ,
    String
    ,
    Double
    , or
    Any
    , even privately. Prefer the domain type's companion/factory direction, such as
    BarcodeFormat.Companion.fromFormat(format: Int)
    , over
    Int.toBarcodeFormat()
    .
  • Add collection extension functions only when the collection has real domain behavior, such as validation across elements, deduplication, ordering, batching, caching, nonempty checks, or error aggregation. If the receiver is a concrete
    Array<SomeDomainType>
    or
    List<SomeDomainType>
    and the body is mostly
    map { ... }
    , keep it in caller code.
  • When a Kotlin converter returns or accepts Android platform
    Int
    constants, apply the matching AndroidX/Java/Kotlin annotation where the API supports it, such as a CameraX
    @ImageCapture.FlashMode
    -style annotation on the function, return value, or parameter. Check the annotation target and retention before placing it; do not annotate blindly when the platform API exposes only a plain
    Int
    .
  • When converting from an annotated platform
    Int
    , put the annotation on the
    format: Int
    parameter if the annotation target supports parameters. For example, prefer
    BarcodeFormat.Companion.fromFormat(@SomeBarcodeFormat format: Int)
    over an untyped primitive input.
  • 类默认保持 final。Kotlin 中的类默认就是 final,除非标记为
    open
    ;除非确实需要继承,否则保持默认即可。
  • 纯值对象优先使用
    data class
    ,有主资源或带生命周期的对象使用普通类。
  • 对于封闭的结果族,优先使用
    sealed interface
    或
    sealed class
    。
  • 使用
    require
    、
    check
    或特定的异常来处理无效输入和无效状态。不要对用户可感知的失败静默无操作。
  • 保持 Java interop 的显式性。在可行的情况下,将平台类型转换为 Kotlin 类型后再通过公共 API 暴露。
  • 避免使用
    !!
    ,除非在狭窄的边界场景中,且之前的检查已明确保证了不变量。优先使用提前返回、
    requireNotNull
    或类型化状态。
  • 在多行控制流中优先使用显式的
    return
    语句。不要为了让代码看起来像表达式而将
    return
    提到多行
    try
    /
    catch
    、
    if
    、
    when
    或 lambda 之外;应使用
    try { return value } catch (...) { return fallback }
    的形式。
  • 在多行 lambda 中,使用带标签的返回,例如
    return@map value
    。仅对于真正的单表达式 lambda(如
    items.map { it.toString() }
    )可以省略标签;不要写
    items.map { return@map it.toString() }
    这种冗余的代码。
  • 对于无歧义的单表达式 lambda,优先使用内联简写:
    formats.map { it.toMLKitFormat() }
    ,而非多行的
    map { format -> format.toMLKitFormat() }
    。如果外层或嵌套 lambda 已经使用了
    it
    ,则不要使用
    it
    ;在嵌套 lambda 或需要清晰度的情况下,应命名参数。
  • 将文件名视为范围约定。
    HybridDataScanner.kt
    应该只实现
    HybridDataScanner
    ;不应同时包含 Android 工具类、几何转换、listener 或扩展工具。
  • 每个文件只包含一个顶层实现类型。不要在主类型下方放置次要类、接口、枚举、选项适配器、协调器、委托或辅助类型。如果一个辅助逻辑值得单独定义类型,它就值得拥有自己的文件。
  • 每个文件只包含一个聚焦的扩展/转换逻辑。不要创建一个包罗万象的扩展文件来存放所有辅助扩展。将转换逻辑拆分到命名文件中,例如
    Barcode+toScannedCode.kt
    、
    TargetBarcodeFormat+toMLKitFormat.kt
    和
    BarcodeFormat+fromMLKitBarcodeFormat.kt
    ,并在适当的情况下使用
    internal
    可见性。
  • 绝对不要在
    Hybrid*
    实现文件或其他主实现文件中放置扩展
    fun
    、
    val
    或
    var
    声明,即使它们是私有的、很小的、或仅在该文件中使用。将所有扩展放在单独的命名为
    Type+operation.kt
    的扩展/转换文件中,以保持代码拆分、可维护性和未来的差异对比清晰。
  • 不要仅仅因为 Kotlin 允许,就将私有顶层扩展属性/函数放在主类下方。伴生对象扩展、枚举/平台映射、条码格式转换以及 ML Kit/CameraX 适配器都应放在各自的命名文件中。
  • 仅当私有顶层辅助函数/属性非常小、非扩展,且仅用于支持该文件的主类型时,才将它们放在同一文件中。
  • 将 Android 前置检查和平台样板代码从
    Hybrid*
    工厂和实现方法中抽离出来。权限检查、清单功能/权限验证、
    PackageManager
    能力检查、服务可用性检查以及类似的设置防护逻辑应放在聚焦的辅助文件中;HybridObject 的调用点应保持一两行的简洁度。
  • 不要为这些辅助逻辑创建宽泛的
    Utils.kt
    文件。根据平台类型或领域检查来命名文件,并确保每个辅助逻辑只聚焦于一个行为。
  • 将行数作为评审信号:只有在满足“每个文件一个类型”和“工厂中无辅助逻辑”的规则后,文件行数在 300 行左右才是可接受的。由辅助逻辑、转换或 Android 粘合代码导致的文件过大是设计问题。
  • 当转换仅读取单个元素时,将转换方法定义在元素类型上,而非
    List
    或数组类型上。优先在调用处使用
    TargetBarcodeFormat.toMLKitFormat()
    加
    formats.map { it.toMLKitFormat() }
    的方式,而非定义
    Array<TargetBarcodeFormat>.toMLKitFormats()
    。
  • 不要在宽泛/通用的接收者类型(如
    Int
    、
    String
    、
    Double
    或
    Any
    )上定义领域转换,即使是私有的也不行。优先使用领域类型的伴生对象/工厂方法来做转换,例如
    BarcodeFormat.Companion.fromFormat(format: Int)
    ,而非
    Int.toBarcodeFormat()
    。
  • 仅当集合具有真实的领域行为时,才添加集合扩展函数,例如跨元素验证、去重、排序、批处理、缓存、非空检查或错误聚合。如果接收者是具体的
    Array<SomeDomainType>
    或
    List<SomeDomainType>
    ,且函数体主要是
    map { ... }
    ,则将其保留在调用方代码中。
  • 当 Kotlin 转换器返回或接受 Android 平台的
    Int
    常量时,如果 API 支持,应添加对应的 AndroidX/Java/Kotlin 注解,例如在函数、返回值或参数上添加 CameraX 风格的
    @ImageCapture.FlashMode
    注解。添加前请检查注解的目标和保留策略;如果平台 API 仅暴露普通
    Int
    ,不要盲目添加注解。
  • 当从带注解的平台
    Int
    转换时,如果注解目标支持参数,请将注解放到
    format: Int
    参数上。例如,优先使用
    BarcodeFormat.Companion.fromFormat(@SomeBarcodeFormat format: Int)
    ,而非无类型的原始输入。

Nitro Notes

Nitro 注意事项

  • Kotlin HybridObject implementations must extend the generated
    Hybrid*Spec
    class and include
    @Keep
    plus
    @DoNotStrip
    .
  • Use
    Promise.async
    for suspending or I/O work and
    Promise.parallel
    for CPU-bound synchronous work.
  • Avoid manually creating or passing around
    Promise<T>
    instances. Prefer
    Promise.async
    ,
    Promise.parallel
    ,
    Promise.resolved
    , and
    Promise.rejected
    ; use a manual Promise only for real native completion/listener/callback bridges that cannot be wrapped as suspend APIs.
  • For general callback APIs such as Google
    Task<T>
    , first write a generic suspend adapter in its own extension file, such as
    Task+await.kt
    , then call it from
    Promise.async { task.await() }
    . Do not hand-wire
    addOnSuccessListener
    /
    addOnFailureListener
    /
    addOnCanceledListener
    into public HybridObject methods.
  • Use
    Promise<Unit>
    for
    Promise<void>
    .
  • Access
    NitroModules.applicationContext
    lazily and fail explicitly if it is unavailable.
  • Generated properties are synchronous JS entry points. Redesign them as methods or listeners if they need Android thread affinity or async work.
  • Kotlin HybridObject 实现必须继承生成的
    Hybrid*Spec
    类,并包含
    @Keep
    和
    @DoNotStrip
    注解。
  • 对于可挂起或 I/O 工作,使用
    Promise.async
    ;对于 CPU 密集型同步工作,使用
    Promise.parallel
    。
  • 避免手动创建或传递
    Promise<T>
    实例。优先使用
    Promise.async
    、
    Promise.parallel
    、
    Promise.resolved
    和
    Promise.rejected
    ;仅当原生完成/listener/回调桥接无法封装为 suspend API 时,才手动创建 Promise。
  • 对于通用回调 API(例如 Google 的
    Task<T>
    ),首先在单独的扩展文件中编写通用的 suspend 适配器,例如
    Task+await.kt
    ,然后通过
    Promise.async { task.await() }
    调用。不要在公共 HybridObject 方法中手动编写
    addOnSuccessListener
    /
    addOnFailureListener
    /
    addOnCanceledListener
    的绑定逻辑。
  • 使用
    Promise<Unit>
    对应
    Promise<void>
    。
  • 惰性访问
    NitroModules.applicationContext
    ,如果不可用则显式失败。
  • 生成的属性是同步的 JS 入口点。如果它们需要 Android 线程亲和性或异步工作,应将其重新设计为方法或 listener。