kmp-architecture-footguns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Kotlin Multiplatform architecture and language footguns

Kotlin Multiplatform 架构与语言陷阱

25 traps mined from a production Kotlin and Compose Multiplatform app, one short file each under
references/
. Every file gives the working pattern, the Traps (the specific ways it fails in practice, and why), and a Verifying it section with commands to run against the user's own tree.
How to use this index. Match the code about to be written, or the symptom being chased, against the entries below: each names its topic and the symptom that should send you to it. Read every file that plausibly applies with the Read tool before proposing code or a fix; each is under 150 lines. Where a Verifying command names the source project's paths, substitute the equivalent paths in the user's tree.
Cross-references. A backticked trap name inside a file that is not listed here lives in a sibling area skill: glob
../*/references/<name>.md
to open it.
从一个使用 Kotlin 和 Compose Multiplatform 的生产级应用中提炼出的 25 个陷阱,每个陷阱对应
references/
下的一个简短文件。每个文件都会给出可行模式、陷阱(它在实际场景中失败的具体方式及原因),以及一个 验证 部分,列出可针对用户自己的项目目录运行的命令。
如何使用此索引。 将准备编写的代码或正在排查的症状与下面的条目进行匹配:每个条目都说明了主题,以及应当由该症状引导你查阅的内容。提出代码或修复方案前,使用 Read 工具阅读所有可能适用的文件;每个文件都不到 150 行。如果验证命令提到了源项目的路径,请替换为用户项目树中的对应路径。
交叉引用。 如果某个文件中的反引号陷阱名称未在此列出,则它位于同级的领域 skill 中:使用 glob
../*/references/<name>.md
打开它。

Module structure, dependency injection and architecture

模块结构、依赖注入与架构

  • kmp-module-split-for-packaging
    — Split one Kotlin Multiplatform UI module into a shared app LIBRARY plus thin per-platform launcher modules — an Android application module that only packages, and a JVM/desktop module that owns main() and hands off to a public function in the library. Reach for it when the Android Gradle Plugin refuses to let your app module also be a multiplatform target, when packaging config and shared UI are tangled in one build script, or when a resource accessor class stopped generating after a module became a library.
  • foss-vs-proprietary-module-pairs
    — Ship one codebase in two forms — a full build carrying a proprietary or credentialed integration and an open build carrying a no-op stub — using twin modules with an identical public API selected by a Gradle property rather than product flavors, which do not exist for non-Android multiplatform targets. Reach for it when a tracking, casting, or paid-service dependency must be absent from an open-source build, when call sites are littered with build-flavor branches, or when the "clean" build still pulls the proprietary artifact through a transitive path.
  • clean-arch-kmp-readiness
    — Lay out a Kotlin Multiplatform app in layers that actually hold — a domain module carrying interfaces and models, repository implementations kept internal to the data module, and one module per external integration so a breaking service cannot spread — plus how to verify each boundary with a grep instead of trusting the diagram. Use when starting a multiplatform app, when splitting a monolithic module, or when platform types have started appearing in shared feature code.
  • no-use-case-layer-decision
    — Decide whether a mid-size app needs an interactor or use-case tier at all, how a clean boundary survives without one (repository interfaces plus pure mapping functions), how to prove an absence rather than assume it, and the specific signal that says it is finally time to add the tier back. Use when the tier feels like typing with no payoff, when reviewing an architecture that has none, or when the same orchestration has been pasted into a third view model.
  • hilt-to-koin-migration
    — Move dependency injection from an annotation-processed compile-time framework (Hilt/Dagger) to the multiplatform runtime container (Koin) — the mechanical mapping for providers, view models and qualifiers, what happens to assisted injection, and the two failure modes the migration introduces: a graph that no longer fails at compile time, and a module definition that blocks the thread starting the container. Use when planning the migration, when a binding resolves to nothing at runtime after it, or when app start got slower afterwards.
  • koin-viewmodel-scoping-traps
    — Wiring view models through a runtime dependency-injection container without losing track of their lifetime — the service-locator base class and what it costs, annotation-based definitions that compile but register nothing, which store owner each accessor addresses and why that (not the accessor's spelling) decides whether two screens share an instance, and what registering a view model as a process-wide singleton makes you responsible for. Use when a view model resolves to a fresh instance that should have been shared, when a lookup fails at runtime for a class that is clearly annotated, or when state vanishes between screens.
  • repository-resource-flow-pattern
    — An envelope family for repository results — a remote wrapper, a local wrapper with a loading state, and a payload-free variant — plus the wrap-side and collect-side helpers that stop every view model from hand-writing the same branch, where the mapping from transport model to domain model belongs, and the one thing the envelope must never swallow. Use when designing repository return types, when error handling has drifted apart between screens, or when a cancelled screen reports a failure it never had.
  • kmp-gradle-settings-catalog
    — Settings-file patterns for a many-module Kotlin Multiplatform repo — mapping deeply nested in-repo directories onto flat Gradle project paths, turning on typesafe project accessors and knowing how they mangle names, declaring repositories in the two places that need them, and pinning one transitive artifact repo-wide for a conflict that only shows at runtime. Reach for it when Gradle reports a project that "does not exist" from a module you never edited, when a project accessor will not resolve, or when a repository you added is invisible to plugin resolution.
  • kmp-git-submodule-module-mapping
    — Consume a git submodule as a set of Gradle modules in a multiplatform repo — mapping its nested directories onto flat project paths, making the recursive clone a hard prerequisite instead of tribal knowledge, and enabling submodules in every continuous-integration job that configures the build. Reach for it when a fresh clone fails with a project that "does not exist", when a build passes locally but fails on a runner, or when shared code changes vanish because the recorded submodule pointer was never moved.
  • two-vendors-one-package-kmp-api-skew
    — Two vendors ship the same package name at different versions into different source sets of one multiplatform build, so a member function that one vendor has already turned into a top-level extension resolves on exactly one target — a specific import compiles for Android and fails for desktop, or the reverse. Covers spotting the duplicate coordinate, why a wildcard import is the correct fix here rather than a smell, and the pinning discipline that keeps the pair readable. Use when shared UI code stops compiling on one target only after a routine dependency bump, when an unresolved-reference error names a symbol you can plainly see in the other target's sources, or when two catalog entries carry the same artifact name.
  • kmp-module-split-for-packaging
    — 将一个 Kotlin Multiplatform UI 模块拆分为共享的应用 LIBRARY,以及各平台轻量级启动模块:Android application 模块只负责打包,JVM/desktop 模块持有
    main()
    并将执行转交给 library 中的公共函数。当 Android Gradle Plugin 不允许 app 模块同时充当 multiplatform target、打包配置与共享 UI 混在同一 build script 中,或模块变为 library 后资源访问器类不再生成时,使用此方案。
  • foss-vs-proprietary-module-pairs
    — 使用同一套代码库交付两种形态:完整构建包含专有集成或需要凭据的集成,开源构建则包含 no-op stub;使用公共 API 完全一致的成对模块,并通过 Gradle property 进行选择,而不是使用 product flavor,因为非 Android multiplatform target 并不存在 product flavor。当开源构建必须排除 tracking、casting 或付费服务依赖、调用点充斥着 build flavor 条件分支,或“干净”构建仍通过传递依赖路径拉取专有 artifact 时,使用此方案。
  • clean-arch-kmp-readiness
    — 以真正稳固的分层方式布局 Kotlin Multiplatform 应用:domain module 承载接口和模型,repository 实现保持在 data module 内部,每个外部集成独立为一个 module,避免破坏性服务向外扩散;同时说明如何用 grep 验证每条边界,而不是只相信架构图。启动 multiplatform 应用、拆分 monolithic module,或平台类型开始出现在共享 feature code 中时,使用此方案。
  • no-use-case-layer-decision
    — 判断中型应用是否需要 interactor 或 use-case 层;没有这一层时如何维持干净边界(repository 接口与纯 mapping function);如何证明这一层确实不存在,而不是凭空假设;以及何种明确信号说明终于该加回这一层。当这一层让人觉得只是增加类型负担却毫无收益、评审一个没有该层的架构,或同一编排逻辑已被粘贴到第三个 ViewModel 中时,使用此方案。
  • hilt-to-koin-migration
    — 将依赖注入从基于注解处理、在编译期工作的框架(Hilt/Dagger)迁移到 multiplatform runtime container(Koin):包括 provider、ViewModel 和 qualifier 的直接映射方式,assisted injection 的处理方式,以及迁移引入的两种失败模式:依赖图不再能在 compile time 暴露错误,以及 module definition 阻塞启动容器的线程。规划迁移、迁移后某个绑定在运行时解析为空,或 app 启动随后变慢时,使用此方案。
  • koin-viewmodel-scoping-traps
    — 通过 runtime DI container 接入 ViewModel,同时不丢失对其生命周期的控制:包括 service-locator 基类及其代价、能够编译通过却不注册任何内容的注解式 definition、每个 accessor 实际寻址的是哪个 store owner,以及为何决定两个 screen 是否共享实例的是 store owner 而不是 accessor 的拼写;还说明将 ViewModel 注册为 process-wide singleton 后需要承担什么责任。当 ViewModel 本应共享却每次都解析出新实例、类明明带有注解但运行时 lookup 失败,或状态在 screen 之间消失时,使用此方案。
  • repository-resource-flow-pattern
    — 一组用于表示 repository 结果的 envelope:remote wrapper、带 loading state 的 local wrapper,以及不含 payload 的 variant;同时提供 wrap-side 和 collect-side helper,避免每个 ViewModel 都手写相同分支,说明 transport model 到 domain model 的 mapping 应放在哪里,以及 envelope 绝不能吞掉的那一点。设计 repository 返回类型、不同 screen 之间的错误处理发生漂移,或已取消的 screen 报告了根本不存在的 failure 时,使用此方案。
  • kmp-gradle-settings-catalog
    — 面向多 module Kotlin Multiplatform repo 的 settings-file 模式:将深层嵌套的 repo 内目录映射到扁平的 Gradle project path,启用 typesafe project accessor 并了解其名称变换规则,在两个需要声明 repository 的位置都进行声明,并针对只在运行时显现的冲突,在整个 repo 中固定一个 transitive artifact。当 Gradle 从一个你从未编辑过的 module 报告 project “does not exist”、project accessor 无法解析,或添加的 repository 对 plugin resolution 不可见时,使用此方案。
  • kmp-git-submodule-module-mapping
    — 在 multiplatform repo 中将 git submodule 作为一组 Gradle module 使用:将其嵌套目录映射到扁平的 project path,把 recursive clone 变成硬性前置条件,而不是依赖团队口口相传的隐性知识,并在每个配置构建的 CI job 中启用 submodule。新克隆因 project “does not exist”而失败、本地构建通过但 runner 失败,或记录的 submodule pointer 未更新导致共享代码变更丢失时,使用此方案。
  • two-vendors-one-package-kmp-api-skew
    — 两个 vendor 将同名 package 以不同版本交付到同一个 multiplatform build 的不同 source set,导致一个 vendor 已提升为 top-level extension 的 member function 恰好只在一个 target 上可解析:某个具体 import 在 Android 上可以编译,在 desktop 上却失败,反之亦然。涵盖如何发现重复坐标、为何此处 wildcard import 是正确修复而不是坏味道,以及让这一对依赖保持可读所需的版本固定纪律。当共享 UI 代码只在某个 target 上于例行依赖升级后停止编译、unresolved-reference 错误指向一个在其他 target 源码中清晰可见的符号,或两个 catalog 条目使用相同 artifact name 时,使用此方案。

Kotlin and multiplatform language traps and utilities

Kotlin 与 multiplatform 语言陷阱及实用工具

  • levenshtein-fuzzy-match-pure-kmp
    — Match a string against a candidate list with a two-row edit-distance loop and no dependency, so the matcher lives in shared multiplatform code. Covers the two-row memory shape, normalizing before comparing, a similarity threshold that refuses rather than returning the least-bad candidate, picking a top-N without corrupting the indices, and the cases where fuzzy matching is the wrong tool. Use when a title-to-title lookup picks a confidently wrong candidate, when a "top 3" helper returns indices that point at the wrong rows or at -1, or when a matcher cannot move into shared code because the library it uses is platform-only.
  • kmp-html-entity-decoder
    — Decode named, hexadecimal and decimal character entities in shared multiplatform code, where the platform's own markup helpers are unavailable. Covers the named table, the two numeric passes, the range check that keeps an out-of-range code point from ending the operation, why running the passes in one order over-decodes, and the rule that decoding happens once and at a boundary. Use when entity text such as
    &#39;
    or
    &amp;
    reaches the screen undecoded, when text decoded twice loses characters a user typed, or when a large code point stops the parse.
  • kotlinx-datetime-helper-kit
    — Wrap the multiplatform date-time library's instant-to-local-date-time conversions in a few named helpers — now, epoch converters, comparisons, a shifted-window helper and a relative "time ago" formatter — so call sites read as intent instead of ceremony. Covers what each wrapper must pin explicitly, and the trap family behind it: a helper reading one time zone while persistence reads another, arithmetic done on wall-clock types, a formatter that can only run during composition, and a parse failure that returns a legal value. Use when stored timestamps come back shifted by the device's offset, when a duration is wrong only around a clock change or only for some users, or when a relative label stays stale on screen.
  • bitmask-event-wrapper
    — Wrap an integer flag set handed up from a lower layer in a single-field value class exposing contains and containsAny, so call sites stop writing raw bitwise tests against library constants. Covers keeping the wrapper allocation-free, why the flag constants must travel with it, the difference between "any of these bits" and "all of these bits", and what happens when a non-flag constant is passed to a flag test. Use when the same bitwise expression is copied across call sites, when a flag test is written as an equality check, or when a wrapper type exists but nothing ever calls it.
  • empty-sentinel-instance
    — Give a model a canonical empty instance on its companion object so its holders can declare the field non-null, instead of threading a nullable through every layer. Covers when this genuinely removes a whole family of null checks and when it only adds a second check beside the one already there, the emptiness predicate that has to ship with it, keeping the sentinel out of persistence and out of rendered lists, and where a nullable is the honest signal. Use when call sites test both for null and for the sentinel, when an empty-keyed row appears in storage, or when a list renders one blank entry at startup.
  • jvmname-disambiguate-erased-overloads
    — Resolve two extension functions that differ only in their generic receiver's type argument and so compile to a single JVM method, using @JvmName on one of them. Covers what the annotation changes, why it beats renaming the Kotlin function, and what non-Kotlin callers see afterwards. Use when the compiler reports a platform declaration clash between declarations you can plainly see are different, when adding a second converter over the same collection type breaks a file that compiled yesterday, or when a Java caller cannot find a function every Kotlin caller uses.
  • model-entity-mapping-extension-layer
    — Put every conversion between transport payloads, domain models and persistence rows in dedicated files of pure extension functions — one direction per function, no suspending work, no logging, nothing else in the file — and keep the layer honest with a grep. Use when conversion code is spreading into data classes, data-access objects or service clients, when a field turns out to be holding a value that belongs to a different field, or when re-reading a row wipes a flag the user set.
  • marker-interface-nested-enum-polymorphism
    — Render one heterogeneous list with one composable by tagging unrelated classes with an empty interface, plus a nested enum each implementor answers where the renderer has to branch. Covers when a tag beats a sealed hierarchy, the exhaustiveness you give up in exchange, and the ways an item silently stops rendering. Use when the items come from modules that cannot be sealed into one file, when a newly added item type appears as a blank row nobody noticed, or when two tags want the same accessor name and one class needs both.
  • expect-actual-composable-capability
    — Expose a device capability to shared Compose code as a @Composable expect function, with a full implementation on the platform that has it and a stub that returns the neutral value on the platform that does not. Covers the three shapes these take — a measurement, an effect with an undo, and a subscription read as state — and why a stub must still be correct. Use when shared UI needs a window measurement, a keep-awake flag or a windowing-mode state, when one platform stops the app the first time a screen paints, or when a shared screen behaves as if a capability is off on a platform that has it.
  • small-collection-utilities
    — Four small helpers worth carrying in a shared module, each with the one way it misleads — a symmetric set difference for diffing two id sets, a position index for constant-time lookups, a tolerant parse/serialize pair for timestamped tokens that returns null instead of throwing, and a translator that rewrites an external link into your own scheme. Use when a diff reports every item as changed, when a position lookup returns the wrong index for a repeated element, when one malformed line takes down a whole screen, or when pasting a link into a search box searches for the link.
  • identity-compare-immutable-setting
    — Bundle a multi-field setting into one immutable value and hand it to a hot consumer as a supplier, so the consumer asks "has this changed?" with a single reference comparison instead of diffing N numbers per buffer — and can never observe the fields half-updated. Use when a per-buffer or per-frame consumer has to react to a user setting, or when a setting made of several fields is read inconsistently.
  • restricted-marker-is-not-an-opt-in
    — Tell an opt-in marker from a restricted-to marker before adding suppressions — the first is enforced by the compiler and demands acknowledgement, the second demands nothing and means something different and worse; includes how to read which one an API carries straight out of the cached artifact, and why both conventions coexist in one library. Use when an
    @OptIn
    looks necessary but the same API compiles without it elsewhere, when the IDE offers a suppression for an annotation you have not read, or when deciding whether a library call is safe to depend on.
  • string-resource-format-limits
    — The multiplatform resource formatter substitutes plain positional placeholders and nothing else — no flags, no width, no escaped percent — so padding, rounding, units and symbols belong in code and the resource only ever joins already-formatted pieces. Covers the same omission in its other two shapes: a raw stored number printed straight to screen, and a date-time library's month names that are constants rather than locale lookups. Use when a format specifier renders verbatim on screen, when a label appears in English regardless of language, or when a screen prints a number in the unit the database happens to store.
  • script-aware-romanization-pipeline
    — Adding a "show pronunciation" or transliteration feature across many non-Latin scripts in a Kotlin Multiplatform module — without pulling in ICU — needs a per-script dispatch built on Unicode block ranges rather than a locale, one line at a time rather than one song at a time, and a hard line between scripts that reduce to a table and the one or two that need a real dictionary. Use when a transliteration result is guessed for the wrong language, an unsupported platform fails outright instead of falling back, or a line that mixes two scripts (an original lyric plus an English aside) picks the wrong one.
  • combining-chars-break-char-literals
    — A Devanagari, Gurmukhi, or other diacritic-marked letter that looks like one glyph in the editor fails to compile as a
    'x'
    character literal with "Too many characters in a character literal," because it is a base letter plus a separate combining mark — two
    Char
    s, not one. Use when a lookup table keyed by
    Char
    needs an entry for a marked or accented letter outside plain Latin, or when per-character text processing garbles exactly the words that carry an accent.
  • levenshtein-fuzzy-match-pure-kmp
    — 使用双行 edit-distance 循环且不依赖任何库,将字符串与候选列表匹配,使 matcher 位于共享 multiplatform 代码中。涵盖双行内存结构、比较前归一化、相似度不足时宁可拒绝也不返回最差候选、在不破坏索引的情况下选取 top-N,以及 fuzzy matching 不适用的场景。当 title-to-title lookup 自信地选中错误候选、“top 3” helper 返回指向错误行或 -1 的索引,或 matcher 因其依赖库仅支持单一平台而无法移入 shared code 时,使用此方案。
  • kmp-html-entity-decoder
    — 在共享 multiplatform 代码中解码命名、十六进制和十进制 character entity,因为平台自身的 markup helper 不可用。涵盖命名实体表、两轮数值解析、防止越界 code point 终止操作的 range check、为何按某一种顺序执行两轮解析会造成 over-decode,以及解码只应在边界处发生一次的规则。当
    &#39;
    或
    &amp;
    等 entity text 未解码便显示到 screen、文本被解码两次后丢失用户输入的字符,或大型 code point 令解析停止时,使用此方案。
  • kotlinx-datetime-helper-kit
    — 用少量命名 helper 封装 multiplatform date-time library 从 instant 到 local date time 的转换:当前时间、epoch 转换、比较、带偏移的窗口 helper,以及相对“time ago” formatter,使调用点表达意图而非样板代码。涵盖每个 wrapper 必须明确固定的内容,以及背后的陷阱家族:helper 读取一个 time zone,而 persistence 读取另一个;对 wall-clock type 执行算术;formatter 只能在 composition 期间运行;parse failure 返回一个看似合法的值。当存储的 timestamp 因设备 offset 返回后发生偏移、duration 仅在 clock change 附近或仅对部分用户出错,或 relative label 长时间停留在 screen 上未更新时,使用此方案。
  • bitmask-event-wrapper
    — 将从下层传上来的整数 flag set 包装为仅含一个字段的 value class,并暴露
    contains
    与
    containsAny
    ,让调用点不再直接针对 library constant 编写原始位运算测试。涵盖如何让 wrapper 免于分配、为何 flag constant 必须随 wrapper 一起传递、“任意这些位”与“所有这些位”的区别,以及把非 flag constant 传给 flag test 时会发生什么。当同一位运算表达式被复制到多个调用点、flag test 被误写成 equality check,或 wrapper type 已经存在却从未被调用时,使用此方案。
  • empty-sentinel-instance
    — 在 model 的 companion object 上提供规范空实例,让持有者可以将字段声明为 non-null,而不必在每一层传递 nullable。涵盖这种做法何时确实消除了一整类 null check,何时只是在既有检查旁又增加一个检查;必须一同提供的 emptiness predicate;如何避免 sentinel 进入 persistence 或渲染列表;以及 nullable 何时才是诚实信号。当调用点同时检查 null 和 sentinel、存储中出现 empty-keyed row,或列表启动时渲染出一条空白项时,使用此方案。
  • jvmname-disambiguate-erased-overloads
    — 使用 @JvmName 解决两个仅在泛型 receiver 的 type argument 上不同、因而编译为同一 JVM method 的 extension function 冲突。涵盖该 annotation 改变了什么、为何它优于重命名 Kotlin function,以及非 Kotlin 调用者之后会看到什么。当 compiler 报告 platform declaration clash,而两个声明显然不同;为同一 collection type 添加第二个 converter 导致昨天还能编译的文件失败;或 Java caller 找不到所有 Kotlin caller 都在使用的 function 时,使用此方案。
  • model-entity-mapping-extension-layer
    — 将 transport payload、domain model 和 persistence row 之间的每一种转换放入专用的纯 extension function 文件:每个 function 只负责一个方向,不包含 suspending work 或 logging,文件中也不放其他内容;并使用 grep 保持这一层的纯粹性。当转换代码扩散到 data class、data-access object 或 service client;某个 field 实际承载了本应属于另一个 field 的值;或重新读取某一行会清除用户设置的 flag 时,使用此方案。
  • marker-interface-nested-enum-polymorphism
    — 通过使用 empty interface 标记彼此无关的类,再让每个 implementor 回答一个 nested enum,使 renderer 能够据此分支,从而用一个 composable 渲染 heterogeneous list。涵盖 tag 何时优于 sealed hierarchy、采用这种方式会放弃哪些 exhaustiveness,以及 item 悄然停止渲染的方式。当 items 来自无法 sealed 到同一文件的 module、新增 item type 显示为无人注意的空白 row,或两个 tag 想使用同一 accessor name 而其中一个 class 同时需要二者时,使用此方案。
  • expect-actual-composable-capability
    — 将设备能力作为 @Composable expect function 暴露给 shared Compose code:在具备该能力的平台上提供完整实现,在不具备该能力的平台上提供返回 neutral value 的 stub。涵盖这些 capability 的三种形态——measurement、带 undo 的 effect,以及以 state 读取的 subscription——并说明 stub 为何仍必须正确。当 shared UI 需要 window measurement、keep-awake flag 或 windowing-mode state;某个平台首次绘制 screen 时导致 app 停止;或 shared screen 在本应具备该能力的平台上表现得像其已关闭时,使用此方案。
  • small-collection-utilities
    — 四个值得放入 shared module 的小型 helper,每个都只有一种会误导人的方式:用于比较两个 id set 差异的对称 set difference;用于 constant-time lookup 的 position index;对带 timestamp 的 token 执行容错 parse/serialize 并在出错时返回 null 而非抛异常的成对工具;以及把 external link 改写为自有 scheme 的 translator。当 diff 报告所有 item 都发生变化、position lookup 对重复元素返回错误索引、一条 malformed line 导致整个 screen 崩溃,或把 link 粘贴到搜索框后搜索的仍是 link 本身时,使用此方案。
  • identity-compare-immutable-setting
    — 把 multi-field setting 打包成一个 immutable value,并以 supplier 形式交给高频 consumer,使 consumer 只需通过一次 reference comparison 判断“是否改变”,而不必为每个 buffer diff N 个数字,并且绝不会观察到只更新了一半的字段。当 per-buffer 或 per-frame consumer 必须响应用户设置,或由多个字段构成的 setting 被不一致地读取时,使用此方案。
  • restricted-marker-is-not-an-opt-in
    — 在添加 suppression 之前,先区分 opt-in marker 与 restricted-to marker:前者由 compiler 强制执行并要求确认,后者不要求任何操作,却表示完全不同且更糟糕的含义;包括如何直接从 cached artifact 判断某个 API 带有哪一种标记,以及为何两种约定会共存于同一 library。当
    @OptIn
    看起来是必需的,但同一 API 在别处无需它即可编译;IDE 为一个你尚未阅读过的 annotation 提供 suppression;或决定是否可以安全依赖某个 library call 时,使用此方案。
  • string-resource-format-limits
    — multiplatform resource formatter 只替换普通的 positional placeholder,除此之外什么也不支持——没有 flags、没有 width,也没有 escaped percent——因此 padding、rounding、units 和 symbols 应放在代码中,resource 只负责拼接已经格式化好的片段。还涵盖其另外两种形态中的同类遗漏:直接显示原始存储数字,以及 date-time library 的 month name 是常量而非 locale lookup。当 format specifier 被原样显示在 screen 上、label 无论使用何种语言都显示英文,或 screen 打印数字时直接采用数据库中恰好保存的单位,使用此方案。
  • script-aware-romanization-pipeline
    — 在 Kotlin Multiplatform module 中为多种非 Latin script 添加“显示读音”或 transliteration 功能,同时不引入 ICU,需要基于 Unicode block range 而非 locale 的 per-script dispatch、逐行而非逐歌曲处理,以及在可归约为表格的 script 与需要真实 dictionary 的一两个 script 之间划出清晰界线。当 transliteration 结果只是针对错误 language 的猜测、unsupported platform 直接失败而没有 fallback,或一行混合两个 script(原始歌词加英文插话)时选错 script,使用此方案。
  • combining-chars-break-char-literals
    — Devanagari、Gurmukhi 或其他带 diacritic mark 的字母在编辑器中看似一个字形,却无法作为
    'x'
    character literal 编译,并报“Too many characters in a character literal”,因为它由 base letter 和单独的 combining mark 组成——是两个
    Char
    ,而不是一个。当以
    Char
    为键的 lookup table 需要为 plain Latin 之外的带标记或带重音字母添加条目,或 per-character 文本处理恰好弄乱包含 accent 的词语时,使用此方案。