mcpField
K
kmp-architecture-footguns

kmp-architecture-footguns

github.com/maxrave-dev开发1.7k86 次安装1 周前更新
代码仓库

关于 kmp-architecture-footguns

面向 Kotlin Multiplatform 开发者的架构与语言排错技能,整理模块设计、依赖注入和跨平台代码中的常见陷阱,并配有针对项目代码的验证命令。

主要功能

  1. 01覆盖多模块架构设计
  2. 02涵盖依赖注入与模块边界
  3. 03整理 Kotlin 多平台语言陷阱
  4. 04提供代码验证命令

适用场景

  1. 01规划或拆分 Kotlin Multiplatform 项目
  2. 02排查依赖注入运行时问题
  3. 03维护跨平台共享代码
  4. 04验证 Gradle 模块与项目边界

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill kmp-architecture-footguns

安装后在 Claude Code 中输入/kmp-architecture-footguns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
bitmask-event-wrapper.md
clean-arch-kmp-readiness.md
combining-chars-break-char-literals.md
empty-sentinel-instance.md
expect-actual-composable-capability.md
foss-vs-proprietary-module-pairs.md
hilt-to-koin-migration.md
identity-compare-immutable-setting.md
jvmname-disambiguate-erased-overloads.md
kmp-git-submodule-module-mapping.md
kmp-gradle-settings-catalog.md
kmp-html-entity-decoder.md
kmp-module-split-for-packaging.md
koin-viewmodel-scoping-traps.md
kotlinx-datetime-helper-kit.md
levenshtein-fuzzy-match-pure-kmp.md
marker-interface-nested-enum-polymorphism.md
model-entity-mapping-extension-layer.md
no-use-case-layer-decision.md
repository-resource-flow-pattern.md
restricted-marker-is-not-an-opt-in.md
script-aware-romanization-pipeline.md
small-collection-utilities.md
string-resource-format-limits.md
two-vendors-one-package-kmp-api-skew.md

详细介绍

Kotlin 多平台架构与语言陷阱

从一个生产环境中的 Kotlin 和 Compose Multiplatform 应用中提炼出的 25 个陷阱,每个陷阱对应 references/ 下的一个简短文件。每个文件都介绍可行模式、陷阱(在实践中导致问题的具体方式及原因),以及通过命令检查用户自己项目的验证方法。

如何使用本索引。 将即将编写的代码或正在排查的症状与下方条目进行匹配:每个条目都列出了对应主题,以及应当查阅该条目的症状。使用 Read 工具阅读所有可能适用的文件,然后再提出代码或修复方案;每个文件都不到 150 行。如果验证命令中写有源项目的路径,请替换为用户项目中的对应路径。

交叉引用。 文件中反引号括起、但未列于此处的陷阱名称,位于同级区域技能中:使用 glob ../*/references/<name>.md 打开该文件。

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

  • kmp-module-split-for-packaging — 将一个 Kotlin Multiplatform UI 模块拆分为共享应用库和精简的平台启动器模块:一个只负责打包的 Android 应用模块,以及一个负责 main() 并将调用交给库中公开函数的 JVM/桌面模块。当 Android Gradle Plugin 不允许应用模块同时作为多平台目标、打包配置与共享 UI 混杂在同一个构建脚本中,或者模块变成库后资源访问器类不再生成时,可以采用这种方式。
  • foss-vs-proprietary-module-pairs — 以两种形式发布同一代码库:完整构建包含专有或需要凭据的集成,开源构建则包含空操作存根。使用公开 API 完全相同的成对模块,并通过 Gradle 属性选择,而不是使用非 Android 多平台目标中不存在的产品风味。当跟踪、投屏或付费服务依赖必须从开源构建中移除,调用处遍布构建风味分支,或者“干净”构建仍通过传递依赖引入专有制品时,可以采用这种方式。
  • clean-arch-kmp-readiness — 将 Kotlin Multiplatform 应用划分为切实有效的层:领域模块承载接口和模型,仓库实现仅在数据模块内部可见,每种外部集成各有一个模块,以免某个服务发生破坏性变更后影响扩散;还介绍了如何用 grep 验证每个边界,而不是盲信架构图。适用于启动多平台应用、拆分单体模块,或平台类型开始出现在共享功能代码中的情况。
  • no-use-case-layer-decision — 判断中等规模应用是否真的需要交互器或用例层,了解没有该层时如何维持清晰边界(仓库接口加纯映射函数)、如何证明某项设计确实不存在而非仅凭假设,以及何时应当重新添加该层。适用于这一层看起来只有输入代码却没有收益、正在审查没有这一层的架构,或同一套编排逻辑已被复制到第三个视图模型中的情况。
  • hilt-to-koin-migration — 将依赖注入从注解处理型编译时框架(Hilt/Dagger)迁移到多平台运行时容器(Koin):介绍 provider、视图模型和限定符的机械式映射、辅助注入的处理方式,以及迁移引入的两种故障模式:依赖图不再于编译时失败,以及模块定义阻塞启动容器的线程。适用于规划迁移、迁移后绑定在运行时解析失败,或迁移后应用启动变慢的情况。
  • koin-viewmodel-scoping-traps — 通过运行时依赖注入容器连接视图模型,同时不丢失对其生命周期的掌控:介绍服务定位器基类及其代价、能够编译却什么都不注册的基于注解的定义、每个访问器对应哪个存储所有者,以及为什么决定两个屏幕是否共享实例的是存储所有者(而不是访问器的名称),还有将视图模型注册为进程级单例后需要承担的责任。适用于视图模型解析出本应共享的新实例、某个明显带有注解的类在运行时查找失败,或状态在屏幕间消失的情况。
  • repository-resource-flow-pattern — 为仓库结果设计一组封装类型:远程封装、本地封装(带加载状态)和无负载变体;介绍封装端和收集端的辅助函数,避免每个视图模型都手写相同的分支、传输模型到领域模型的映射应放在哪里,以及封装绝不能吞掉的内容。适用于设计仓库返回类型、不同屏幕间的错误处理逐渐不一致,或已取消的屏幕报告了从未发生的失败时。
  • kmp-gradle-settings-catalog — 面向多模块 Kotlin Multiplatform 仓库的设置文件模式:将仓库中深层嵌套的目录映射到扁平的 Gradle 项目路径、启用类型安全的项目访问器并了解它们如何改写名称、在需要声明仓库的两个位置分别配置仓库,以及为运行时才会出现的冲突在整个仓库中固定一个传递制品版本。当 Gradle 在你从未编辑过的模块中报告某个项目“不存在”、项目访问器无法解析,或你添加的仓库对插件解析不可见时,可以采用这些模式。
  • kmp-git-submodule-module-mapping — 在多平台仓库中将 git 子模块作为一组 Gradle 模块使用:将其嵌套目录映射到扁平的项目路径、将递归克隆设为硬性前提而非口耳相传的知识,以及在每个配置构建的持续集成任务中启用子模块。当全新克隆后因项目“不存在”而失败、构建在本地通过却在运行器上失败,或共享代码变更消失(因为记录的子模块指针从未更新)时,可以采用这种方式。
  • two-vendors-one-package-kmp-api-skew — 两个供应商将相同包名的不同版本放入同一多平台构建的不同源集中,因此一个供应商已将成员函数改为顶层扩展函数,而另一个尚未更改,导致它只在一个目标平台上解析:某个特定导入在 Android 上编译通过、在桌面端失败,或反之。介绍如何发现重复的坐标、为什么这里正确的修复方式是使用通配符导入而非将其视为代码异味,以及如何规范固定版本以确保这两个版本易于辨认。适用于常规依赖升级后共享 UI 代码只在一个目标平台上无法编译、未解析引用错误提到的符号明明出现在另一目标平台的源码中,或两个目录条目使用相同制品名称的情况。

Kotlin 和多平台语言的陷阱与实用工具

  • levenshtein-fuzzy-match-pure-kmp — 使用双行编辑距离循环匹配候选列表中的字符串,且不依赖任何库,因此匹配器可以放在共享多平台代码中。涵盖双行内存结构、比较前进行规范化、低于相似度阈值时拒绝而非返回最差候选项、选择前 N 个结果时避免索引错乱,以及不适合使用模糊匹配的情况。当标题与标题的查找选中了一个看似可靠、实则错误的候选项时,当“前 3 项”辅助函数返回的索引指向错误行或 -1 时,或者当匹配器因依赖的库仅支持特定平台而无法移入共享代码时,适合使用。
  • kmp-html-entity-decoder — 在共享多平台代码中解码命名、十六进制和十进制字符实体,以应对无法使用平台自带标记辅助函数的情况。涵盖命名实体表、两轮数值转换、避免超出范围的码点中止操作的范围检查、按某种顺序执行转换轮次为何会过度解码,以及只在边界处解码一次的规则。当 &#39; 或 &amp; 等实体文本未经解码就显示在屏幕上时,当文本被解码两次导致用户输入的字符丢失时,或者当较大的码点导致解析停止时,适合使用。
  • kotlinx-datetime-helper-kit — 为多平台日期时间库的 instant 到 local-date-time 转换封装几个具名辅助函数——当前时间、epoch 转换器、比较、时间窗口偏移辅助函数和相对“多久以前”格式化器——让调用处表达意图,而非充斥样板代码。涵盖每个封装函数必须明确指定的内容,以及背后的常见陷阱:辅助函数读取一个时区,而持久化读取另一个时区;对墙上时钟类型进行算术运算;只能在组合期间运行的格式化器;以及返回合法值的解析失败。当存储的时间戳读取后受设备时区偏移影响而发生偏移时,当时长只在时钟变更前后或对某些用户而言出错时,或者当相对时间标签在屏幕上一直不更新时,适合使用。
  • bitmask-event-wrapper — 将从较低层传来的整型标志集封装在单字段值类中,并提供 contains 和 containsAny,让调用处不再针对库常量编写原始位运算判断。涵盖如何让封装类型不产生额外分配、标志常量为何必须与封装类型一同传递、“任意一个这些位”和“所有这些位”的区别,以及将非标志常量传给标志判断时会发生什么。当相同的位运算表达式被复制到多个调用处时,当标志判断被写成相等性检查时,或者当封装类型已经存在却从未被调用时,适合使用。
  • empty-sentinel-instance — 在模型的 companion object 上定义一个规范的空实例,让持有者可以将字段声明为非空,而不必让可空类型层层传递。涵盖这种做法何时能真正消除整类空值检查,何时只是在已有检查旁边又加了一项检查、必须随之提供的空值判定谓词、如何避免将哨兵值写入持久化存储或渲染到列表中,以及可空类型何时才是恰当的信号。当调用处既检查 null 又检查哨兵值时,当存储中出现空键行时,或者当列表在启动时渲染出一个空白条目时,适合使用。
  • jvmname-disambiguate-erased-overloads — 使用 @JvmName 标注其中一个扩展函数,解决两个扩展函数仅在泛型接收者的类型实参上不同、因而编译为同一个 JVM 方法的问题。涵盖注解会改变什么、为何它比重命名 Kotlin 函数更合适,以及之后非 Kotlin 调用方会看到什么。当编译器报告平台声明冲突,但你明明看得出声明并不相同时,当在同一种集合类型上添加第二个转换器导致昨天还能编译的文件报错时,或者当 Java 调用方无法找到每个 Kotlin 调用方都在使用的函数时,适合使用。
  • model-entity-mapping-extension-layer — 将传输负载、领域模型和持久化行之间的所有转换都放入专用文件中的纯扩展函数:每个函数只处理一个方向,不执行挂起操作,不记录日志,文件中也不放其他内容;再用 grep 确保这一层名副其实。当转换代码散落到数据类、数据访问对象或服务客户端中时,当发现某个字段存储了本应属于另一字段的值时,或者重新读取一行数据会覆盖用户设置的标志时,适合使用。
  • marker-interface-nested-enum-polymorphism — 为互不相关的类实现一个空接口作为标记,并让每个实现类提供一个嵌套枚举,指示渲染器应在哪种情况下分支,从而用一个可组合函数渲染一个异构列表。涵盖何时标记优于密封层级、这样做会失去哪些穷尽性保证,以及某个条目如何在不知不觉中停止渲染。当条目来自无法密封到同一文件中的模块时,当新添加的条目类型显示为空白行却无人察觉时,或者当两个标记需要同名访问器、而一个类必须同时实现两者时,适合使用。
  • expect-actual-composable-capability — 通过 @Composable expect 函数将设备能力暴露给共享 Compose 代码;在具备该能力的平台上提供完整实现,在不具备该能力的平台上提供返回中性值的存根。涵盖这类函数的三种形式——测量值、带撤销操作的副作用,以及以状态形式读取的订阅——以及存根为何也必须正确。当共享 UI 需要窗口测量值、保持唤醒标志或窗口模式状态时,当某个平台在屏幕首次绘制时就会停止应用时,或者当共享屏幕表现得像是某个平台上的某项能力已关闭,而该平台实际上具备这项能力时,适合使用。
  • small-collection-utilities — 适合放入共享模块的四个小型辅助函数,每个函数都说明了一种容易误导人的情况:用于比较两个 ID 集合的对称差、用于实现常数时间查找的位置索引、用于解析/序列化带时间戳令牌且出错时返回 null 而非抛出异常的容错函数,以及将外部链接改写为自有 scheme 的转换器。当差异比较报告所有条目都已更改时,当重复元素的位置查找返回错误索引时,当一行格式错误的数据导致整个屏幕崩溃时,或者当将链接粘贴到搜索框中却变成搜索这个链接时,适合使用。
  • identity-compare-immutable-setting — 将多字段设置捆绑为一个不可变值,并以 supplier 形式传给高频调用方,让调用方只需比较一次引用来判断“它是否已改变”,而不是针对每个缓冲区比较 N 个数字;同时确保调用方绝不会观察到只更新了一半的字段。当每个缓冲区或每一帧的调用方都必须响应用户设置时,或者由多个字段组成的设置被读取时前后不一致时,适合使用。
  • restricted-marker-is-not-an-opt-in — 添加抑制标注前,先区分 opt-in 标记和 restricted-to 标记:前者由编译器强制执行并要求确认,后者没有任何要求,含义也不同且后果更严重;还包括如何直接从缓存的构件中读取 API 带有哪种标记,以及为何同一个库会并存两种约定。当 @OptIn 看起来必不可少、但同一个 API 在其他地方无需它也能编译时,当 IDE 为尚未读过的注解建议添加抑制标注时,或者在判断依赖某个库调用是否安全时,适合使用。
  • string-resource-format-limits — 多平台资源格式化器只会替换普通位置占位符,不支持其他功能——没有标志、宽度或转义百分号——因此填充、舍入、单位和符号都应在代码中处理,资源中只能组合已格式化的部分。还涵盖另外两种形式中的相同疏漏:直接将原始存储数字显示在屏幕上,以及日期时间库的月份名称是常量而非本地化查询。当格式说明符原样显示在屏幕上时,当标签不论语言设置如何都显示为英文时,或者当屏幕使用数据库中碰巧存储的单位显示数字时,适合使用。
  • script-aware-romanization-pipeline — 在 Kotlin Multiplatform 模块中为多种非拉丁文字添加“显示读音”或转写功能而不引入 ICU,需要基于 Unicode 区块范围(而非地区设置)进行逐文字调度、逐行处理(而非逐曲处理),并明确区分可通过查表完成的文字与需要真正词典的一两种文字。当转写结果猜错语言时,当不受支持的平台直接失败而非回退时,或者当混合两种文字的行(原文歌词加英文旁注)选错处理方式时,适合使用。
  • combining-chars-break-char-literals — 在编辑器中看起来像一个字形的天城文、古木基文或其他带变音符号的字母,作为 'x' 字符字面量时会因“字符字面量中字符过多”而编译失败;这是因为它由一个基本字母和一个单独的组合标记组成,也就是两个 Char,而不是一个。当以 Char 为键的查找表需要包含普通拉丁字母以外的带标记或重音字母时,或者当逐字符文本处理恰好会破坏含重音符号的单词时,适合使用。

评论

支持 Markdown