这篇博客记录我用 Kotlin Multiplatform 开发一个双端 GitHub 客户端(iOS + Android)的全过程------从产品定位、UI 设计、技术选型到架构分层与 AI 协作流程。不铺陈 API 细节,重点讲决策与取舍。
为什么做这个项目
这个项目想是我第一次尝试 用 AI 开发一个完整的 App。作为一个专业开发者,我对成品质量有明确要求:
- 产品设计贴近真实需求,不是凑功能的玩具;
- UI 交互精美而又简约;
- 架构设计合理,能长期演进;
- 同时支持多端(iOS / Android,未来再扩展 Harmony);
- 用跨端技术 + 组件化,把代码量压下来。
产品定位是以浏览作者关注的仓库及其内容为主,给用户一个沉浸式的浏览体验。主要功能:首页(我的仓库 / Starred / 最近访问)、搜索,以及仓库详情(概览 / 代码 / 提交 / Issues / PR / 发布)。
核心技术诉求是尽可能多地复用代码逻辑,并且保持原生体验。于是跨端技术我选了 KMP(Kotlin Multiplatform):一份高质量代码同时服务两端,UI 则走各端原生技术栈(比如 iOS 是 SwiftUI),原生体验不打折。
同时,我不希望用 AI 生成零散代码,而是让 AI 全链路参与,并沉淀一套开发类似架构 App 的通用工作流。于是我一边开发一边总结,在开发的同时形成 AI 开发脚手架------这也是这个项目最大的副产物。
效果展示
| 首页 | 详情 | 导航 | 文件浏览 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |

产品设计:范围与取舍
产品范围是第一个决策。GitHub 功能太多,不可能全做。我的取舍原则是:
- 锁定核心浏览场景:仓库(首页 / 搜索 / 详情)、代码、提交、Issues、PR、发布;
- 暂缓写操作与社交功能:因为浏览是 80% 的使用场景,用最小场景集验证技术闭环。
本项目会采用较新的端侧技术,因此先把一期功能收敛到一个小范围,用最少的投入保证 App 体验。功能范围定义在 docs/功能规划.md,以「已实现 / 待实现 / 待规划」三种状态跟踪每个功能点;docs/导航设计.md 则用一张导航图直观呈现页面之间的跳转关系,让范围边界一目了然。
UI 设计工作流:从原型到双端落地
UI 设计是我花了不少心思的地方,最大的风险是两端视觉漂移。为此我定了三个设计原则:
- 定一套设计基调,保证全局体验一致:跨页面、跨端观感统一;
- 设计数值与组件标准化:颜色 / 间距 / 字号等全部走 token,组件两端同名映射,杜绝各写各的;
- 页面设计优先参考与复用已有设计:新页面先查已有组件与资产,默认复用不默认新写,防止设计腐化。
具体落到四个约定:
HTML 即设计稿
每屏一个 HTML 文件,由 AI 使用固定模板创建,浏览器可以直接打开看。设计稿放在 design/screens/,按 feature 分目录。
设计值统一走 token
颜色 / 间距 / 字号 / 字重 / 圆角 / 阴影全部定义在 design/tokens/。出新设计稿时优先从这个集合取用适当的值 ,代码里一律引用 token、禁止写死设计值------这是两端视觉一致的根基。
图标约定
图标由 AI 根据场景智能生成,统一以 SVG 存储,原则是能复用则复用。集成资源时,各端再转换为端上支持的格式(iOS 出 PDF、Android 出 VectorDrawable),禁止手抄 SVG path。
组件化约定
同一个组件在三端(HTML 原型、iOS、Android)保持同名、结构一致,并维护一张映射表随时可查。
- 设计前先扫组件列表:能复用不新建,默认复用。
- 设计后主动提取:把页面切成「块」,逐块判定------跨页重复的抽为公共组件(至少 2 个真实使用方),暂无复用线索的留页面私有,加载 / 空 / 错误一律通用。
- 人工可介入:是否强行复用组件,私有组件是否升级公共,由人来确认。
跨端技术:为什么是 KMP
候选是 KMP、React Native、Flutter 三选一。判断基准不是技术热度,而是基于功能诉求、开发成本、用户体验等多因素的综合判断。
核心结论:业务层共享 + 原生 UI 各自治。 双端 UI 各写各的(SwiftUI / Compose),逻辑层只写一套。
- 优势:性能与体验和原生一致;逻辑层高度复用,与 UI 解耦,有利于架构的健康发展。
- 代价:UI 代码要写两遍,开发和验收都有成本。
其他因素:KMP 跨平台兼容性好(甚至支持鸿蒙);额外的包大小和内存占用很小。
双端 UI:声明式范式
前面说 UI 各写各的,那两套 UI 靠什么保持默契?答案是 声明式范式 ------SwiftUI 和 Compose 遵循同一个心智模型:UI 是状态的函数。界面只做「订阅状态 → 按状态渲染」,不自己拼装、不自己维护数据。
这跟命令式的老写法相反:命令式要你一步步「先创建、再赋值、再刷新」;声明式只要描述「状态是这样,界面就该长这样」,状态变了界面自动跟着变。
好处有两个:
- 和架构天然契合:单向数据流里「UI 只订阅状态、只发动作」,正好是声明式的标准姿势,UI 层因此非常薄------只有「状态 → 视图」的映射,没有逻辑;
- 双端写起来像同一门语言:SwiftUI 和 Compose 语法不同,但「组件 + 状态 + 绑定」的概念一一对应,配合组件同名映射,两端可以按同一张设计稿对拍翻译,视觉不容易跑偏。
架构设计:分层与边界
架构是整个项目最关键的决策。我采用了四层模型:
app 平台壳:两端页面 UI + 桥接层
features 各业务模块:自己的状态与动作,不含任何 UI
business 公用业务:跨模块共享的模型、逻辑与状态
infra 底层能力:网络 / 认证 / 存储 / 序列化等
三条硬规则:
- 依赖单向向下、features 互不依赖。任何两个 feature 都要用的东西晋升到 business。约束不是口头约定,而是「违反即编译失败或架构测试(Konsist)失败」。
- 模块暴露面收敛。每个模块只暴露别层需要的类型,内部类型和接口不外泄。
- 桥接层只在 app 。iOS 侧把 KMP 状态流桥接给 SwiftUI 的代码只出现在
app/ios;infra 的 iOS 侧禁止 Kotlin 调 UIKit,界面与平台 UI 一律由 Swift 实现。
基础库设计原则
- 优先复用成熟的 KMP 库:选择较新且稳定的版本,不重复造轮子;
- 底层非 UI 能力一律 Kotlin 封装:两端只面向统一接口,禁止各端直接调用原生接口,保持行为一致;
- UI 能力也以 Kotlin 接口对外:比如 Toast 这类系统能力,两端统一走 Kotlin 门面,内部再由各端原生实现;
- 纯组件(一般是 UI 视图的子类)各端分别实现:这些组件没有共享逻辑,留在两端各自的 UI 代码里,不做跨端统一。
通信机制
页面跳转:导航器 + 契约
先解释几个核心概念:
- 目的地:想去哪个页面(仓库详情、搜索页...);
- 导航事件:业务侧想要跳转时发出的一条「请求单」,上面写着去哪、怎么跳(压栈 / 模态 / 弹窗)、带什么参数。业务自己不会跳,只负责递请求单;
- 导航器:app 层负责统一「照单执行」的组件------收到请求单后,用两端各自的原生方式真正完成跳转;
- 深链:通过 URL 直达某个页面(App 内链接点击、系统外部链接、通知点击)。
整体是一个「契约在 business、执行在 app」的模型。契约就是这套导航请求单的类型定义,只描述「跳去哪、怎么跳、带什么参数」,不关心两端如何实现:
- 契约放 business:导航事件、目的地、弹窗、深链等定义都放在这层。因为这些是各个页面都要发的「请求单」,而页面之间又互不依赖(架构约束),所以契约必须放在一个大家都能依赖的共同下层------谁要跳转都只引用这份契约,不引用目标页面本身;
- 执行放 app:导航器维护几块平台无关状态------当前在哪个 Tab、每个 Tab 各自的页面层级、当前模态(半屏 / 全屏)、当前弹窗;
- 页面只发导航事件:想跳转就发一张请求单,从不直接触碰任何跳转逻辑。
Back 语义统一为「先关弹窗 → 再关模态 → 最后弹栈」。iOS 用 NavigationStack / sheet / alert,Android 用嵌套 NavHost / ModalBottomSheet / Dialog。
深链统一入口:所有 URL(系统深链、App 内链接、通知点击)一律先解析成导航事件,再走同一执行入口。一期不内嵌 WebView,外部链接统一交系统浏览器。
feature ⇄ feature:逻辑下沉 business
架构上 feature 之间是互不依赖的,那它们之间需要协作时怎么办?比如在搜索页点开一个仓库,要跳转到仓库详情页。答案是不直接通信,绕道中间层:
- 要共享的数据:下沉到 business 层(登录态、仓库模型等公共能力),feature 各取所需;
- 要触发的跳转:通过导航事件上抛到 app,由导航器统一导航到目标页面------发起方只引用契约,不引用目标页面本身;
- 要共享的动作:同样下沉到 business 的公用动作,谁需要谁调用。
这样 feature 之间始终保持零依赖,协作关系全部收拢在 business 和 app 两层。
feature ⇄ UI:单向数据流
单向数据流 :数据只会朝着一个方向流动------UI 想改数据,不直接改,而是发一个「动作」给业务层;业务层改好状态后,再把新状态推给 UI 展示。改、读、听是三条分得很清的路:
- 改:UI 只发动作(点登录、拉下一页...),不自己动数据;
- 读:每个页面只认一份状态(单一事实来源),数据怎么组合、从哪来都归业务层管,UI 不保留第二份副本;
- 听:UI 订阅这份状态,业务层一更新就收到通知、重新渲染,自己不用去轮询或同步。
改、读、听三件事分工明确,都围绕同一份状态:
- 改(动作函数) :UI 不直接改状态,而是调用业务层的动作函数(如
onLoadMore、onLogin),业务层处理完后再产出新状态推给 UI; - 读(状态流
StateFlow<State>):不可变聚合结构,UI 只读这一份,更新一律「复制出新状态」替换旧状态; - 听(订阅状态流):UI 订阅这份状态,业务层一更新就收到通知、重新渲染,自己不用去轮询或同步。
另有一条独立的一次性事件通道 (Channel):导航、弹窗这类「做完就消失」的事件,消费后即消失,不写进任何共享状态------它不属于上面「改 / 读 / 听」的状态体系。
这样事件向上、状态向下,UI 与数据源(网络 / 数据库)之间没有任何直接通道。
研发流程
这个项目百分之百采用 AI 研发。为了让 AI 稳定地产出符合规范的代码,我沉淀了一套流程:
文档与规范先行
每个页面先有技术方案再动手(docs/ 下每页一份);同时预先制定并沉淀了一整套规范文档:设计规范、架构规范、各语言编码规范、注释规范等。 文档回答「做什么、怎么做」,规范回答「怎么写算合格」。
设计先行:页面从原型到落地
每个页面在开发前都走完整的设计流程:出 HTML 原型 → 确认设计稿 → 组件 / 资源抽取 → 核对调整。新页面的设计原则有三条:UI 保持简约、风格与整体统一、优先复用已有设计元素(token + 组件),只有当页面被合理分区后出现的新结构,才产出新组件。
六阶段功能开发流程
功能开发被定义为六个阶段:需求澄清 → 技术调研 → 开发规划 → 开发实施 → 测试验证 → 验收交付。每个阶段设「人拍板」门槛------AI 只给建议、产文档,关键决策(功能范围 / 选型 / 任务清单 / 桩去留)一律由用户确认,不擅自动手。
质量保障
测试与 demo 分工明确,由 AI 工作流在开发功能时同步生成。每个任务完成后立即验证,不等最后统一验证:
- 功能模块走单测:业务逻辑层用 kotlin-test + Turbine 断言状态流、Konsist 校验架构分层;
- 页面和组件走 demo:同步维护一个平行壳(假数据 + 双端 UI 副本 + 页面条目),方便随时预览效果。
总结
用 KMP 做到了「一份逻辑共享两端、UI 各自原生」,换来业务代码一份维护、原生体验不打折;代价是 UI 要写两遍,对架构分层要求比较高。
在 AI 时代,写代码已经不需要人了。但如果一切都交给 AI,项目开始会很快,随着迭代维护却会迅速腐化------带来线上风险和流程的不可控。所以要为 AI 研发补上的,恰恰是过去容易被视为「性价比不高」的工程约束:
- 非 AI 时代,架构设计和组件规范很难严格落地,因为靠人协作去执行规范的代价太大;
- AI 不同,它能严格参考规范标准执行,还能自动化生成测试与验证流程------补齐了那些很重要、但让人来做会觉得性价比不高的事;
- 这些事在 AI 开发流程里,重要性被显著放大:AI 自身不保证质量,必须由人来设计流程约束 AI,才能提升工程质量,同时降低人在质量上投入的额外成本(人也会出错)。
所以这个项目最大的收获不是某段代码,而是一套由人设计、靠文档与规范约束、由 AI 严格执行的开发流程,以及沉淀下来的架构设计------它们让 AI 的高效和工程的严谨同时成立。



