【免费下载链接】hammer-editor
A simple tool for building stories.
Hammer 是一款免费开源的跨平台故事编辑器(story editor),支持 Android、iOS、Windows、macOS 和 Linux。本文带你用最少代码、最快路线拆解它的 Clean 分层架构与 Kotlin Multiplatform 模块结构,帮你在第一次打开仓库时就能建立完整地图 🪨
一、先认识 Hammer:一个"数据在你手里"的跨平台故事编辑器
在拆架构之前,先花 30 秒理解这个产品要解决什么问题,架构设计才有落脚点:
- 离线优先(Offline first):完全本地运行,不依赖网页技术,长时间离线写作不丢数据
- 数据透明(Transparent Data):项目就是人类可读的普通文件与文件夹,不锁在云端的黑盒数据库里
- 可选同步:可自部署同步服务器,也可用 Dropbox、Syncthing 等任意方式同步
- 多端一致:手机、平板、桌面端共用同一份业务代码,界面按屏幕自适应
它的核心写作功能围绕四大模块组织:场景(Story)、百科(Encyclopedia,人物/地点/事件)、笔记(Notes)、时间线(Timeline),再加一个能穿透到任意内容的全局搜索。
理解了"数据透明 + 多端一致 + 离线优先"这三个产品目标,你会发现后文的每个架构决策几乎都能直接对应上。
二、Clean 架构总览:六层结构,依赖只允许向下
Hammer 采用的是一套严格约束的 Clean 风格分层。完整规范写在官方文档 docs/ARCHITECTURE.md 中,这是全仓库最重要的架构契约。层级从"最靠近数据"到"最靠近界面"排列为:
Data Sources → Foundation → Repositories → Services → Use Cases → ViewModels其中强制主干只有三层:Data Source → Repository → ViewModel。Services 和 Use Cases 是可选层,只在真实需求出现时才插入,官方明确反对"预判式建层"。
| 层级 | 有状态? | 生命周期 | 必须? |
|---|---|---|---|
| Data Source 数据源 | 无状态 | 工厂创建,每次新实例 | ✅ |
| Foundation 基础原语 | 有状态 | 作用域单例 | 固定集合 |
| Repository 仓库 | 有状态 | 作用域单例 | ✅ |
| Service 服务 | 有状态 | 作用域单例 | ❌ 可选 |
| Use Case 用例 | 无状态 | 工厂创建 | ❌ 可选 |
| ViewModel(Component) | — | 每屏幕一个 | ✅ |
💡 本项目把最高层称为Component(Decompose 组件),等价于其他架构里的 ViewModel,两个词在文档中通用。
Foundation:一个"固定的三件套"
Foundation 层不是随便加东西的地方,而是全数据层公用的固定有状态基础设施,目前只有三个成员:
IdAllocator— 分配全项目单调递增的实体 ID:IdAllocator.ktSyncJournal— 记录等待与服务器对账的脏数据(位于 data/sync/)GlobalSettingsStore— 应用级与服务器设置:GlobalSettingsStore.kt
三者构成无向环的 DAG(GlobalSettingsStore ← SyncJournal ← IdAllocator),这是全仓库中唯一允许有状态组件横向引用同层的地方——正因为集合是手工保持无环的。
决策规则:该加哪一层?
官方文档给出了极简判断法,值得背下来:
- 需要 Use Case?只有当存在无状态业务逻辑/组合时才加;如果只是把一次调用原样转发给 Repository,就别加
- 需要 Service?只有当需要跨两个及以上 Repository协调且带共享状态时才加(Repository 之间禁止互相引用,协调逻辑只能放这里)
- 需要 Foundation 原语?几乎永远不要。"两个 Repository 都需要它"不是理由——那是 Service 的职责
三、实战拆解:一次"场景编辑"如何穿过各层
纸上谈兵不如看真实案例。docs/DESIGN-PATTERNS.md 记录了一次教科书式的分层重构:场景编辑域曾经是约 1300 行、捆绑 12 项职责的SceneEditorRepository,最终被拆成"一个门面 + 三个仓库":
SceneEditorService— 组件层唯一入口,负责写操作编排(在每条命令周围挂上统计、引用索引、同步标记等横切副作用):SceneEditorService.ktSceneRepository— 场景树、结构、路径计算、同步哈希,纯数据:SceneRepository.ktSceneContentRepository— 编辑引擎:内存缓冲区、内容防抖管道、临时缓冲区自动保存:SceneContentRepository.ktSceneMetadataRepository— 场景与项目元数据的持久化
注意一个关键设计:Component 层只能透过SceneEditorService这一个门访问场景编辑域,调用者永远不需要纠结"该注入 repo 还是 service"。而 Service、Use Case、同步层仍然可以直接消费下层 Repository——Service → Repository这条边是合法的,只有最高层被收口。
依赖注入由 Koin 完成,约定很简单:无状态组件(Data Source、Use Case)工厂创建;有状态组件(Repository、Service)作用域单例,且作用域尽量选窄(会话、导航图),别一股脑塞进应用级单例。入口模块见 dependencyinjection/。
四、Kotlin Multiplatform 模块结构:7 个 Gradle 模块各管一摊
在 settings.gradle.kts 中可以看到完整的模块清单。这是理解 KMP 结构的第一张地图:
| 模块 | 职责 | 关键内容 |
|---|---|---|
| base/ | 最底层共享原语 | ID 类型、markdown 工具、http、校验、diff |
| common/ | 核心业务(约 350 个文件) | 数据层六层全家桶 + 组件层 + 同步协议 |
| composeUi/ | 跨平台 UI(约 250 个文件) | Compose Multiplatform 界面组件 |
| android/ | Android 宿主 | Activity、Widget、通知 |
| desktop/ | 桌面宿主 | 窗口管理、系统快捷键、原生集成 |
| server/ | 独立同步服务器 | Ktor + SQLDelight + PostgreSQL |
| wear/ | 手表端(非 F-Droid 构建) | Wear Compose 配对界面 |
| integrationTests/ | 集成测试 | 端到端场景验证 |
模块之间的依赖方向非常克制:base在最底,被所有模块依赖;common承载几乎全部业务逻辑;composeUi消费common的组件;各平台模块(android/desktop/wear)只是薄薄的一层"胶水",负责启动、权限和平台 API 接入。技术栈版本统一收敛在 gradle/libs.versions.toml 中:Kotlin 2.4、Compose Multiplatform、Decompose 3.5(组件框架)、Koin(DI)、Ktor、okio 文件系统抽象。
服务器模块是独立世界:它不复用客户端的 common 代码,而是按自己的轻量分层组织——Routes → Repository → Datasource(见 server/src/main/),数据库层用 SQLDelight 管理 schema 与迁移(server/src/main/sqldelight/)。
五、同一份代码跑五种端:源码集(Source Set)是怎么切分的
模块是"横向"切分,KMP 的源码集则是模块内部的"纵向"切分。以核心模块common为例,它的src/目录长这样:
commonMain— 编译到所有平台的共享代码(业务逻辑全在这里)desktopMain/androidMain/iosMain/jvmMain— 各平台实现与胶水commonTest/desktopTest/iosTest— 各端测试
一个有趣的工程细节:因为 mock 库 mockk 尚不支持 Kotlin/Native,绝大多数单元测试被放在desktopTest源码集里跑(gradlew desktopTest),这是 DEVELOPMENT.md 明确说明的取舍。
测试还承担了架构守卫的职责,例如 PersistedFormatOwnershipTest.kt 强制"每种磁盘文件格式只允许一个 Data Source 拥有"——历史上两个独立的project_data.toml写入方曾互相擦掉对方字段,这条规则就是事故教训固化成的自动化检查。
六、给读源码的你:核心依赖规则速查
官方把最容易踩的坑浓缩成了硬性约束,收藏 docs/ARCHITECTURE.md 即可:
- 只允许向下依赖:任何组件只能引用比它更低的层
- 禁止同层互引(No sibling references):这是保证依赖图天然无环的关键
- 唯一例外:无状态的 Use Case 可以引用其他 Use Case
- Use Case 之间用懒注入(
Provider<T>或 lambda),无状态不足以防止构造循环 - ViewModel 永远不直接碰 Data Source,必须经过 Repository 或更上层
- Foundation 原语是下层共享件,不是兄弟,任何层引用它都算"向下"
- 一种持久化格式只有一个属主,由架构测试强制
七、源码导航清单:按这个顺序读效率最高
最后给出一份最小阅读路径,从产品到代码逐层下钻:
- 📖 架构总纲:docs/ARCHITECTURE.md
- 📖 设计模式目录(深链导航、场景编辑域 API 等范例):docs/DESIGN-PATTERNS.md
- 🗺️ 模块清单与构建入口:settings.gradle.kts、gradle/libs.versions.toml
- 🧩 组件层入口(屏幕栈与导航):components/projectroot/
- 🗄️ 数据层主战场:data/(30+ 个领域子包)
- 🔌 依赖注入装配:dependencyinjection/
- 🖥️ 开发指南与测试哲学:DEVELOPMENT.md
- 📦 构建元数据(版本、渠道、发布脚本):buildSrc/
小结:Hammer 的架构精髓可以浓缩为一句话——用"只向下依赖"的 Clean 分层保证业务代码与平台无关,再用 KMP 的源码集切分把这一份业务代码铺到五种操作系统上。读懂common模块里的六层数据流和base/composeUi的边界,你就掌握了这个代码库 90% 的脉络。
【免费下载链接】hammer-editor
A simple tool for building stories.
相关推荐
一文读懂bRPC:高性能RPC框架的核心模块与架构设计全解析
一文读懂bRPC:高性能RPC框架的核心模块与架构设计全解析 bRPC是百度开源的高性能RPC框架,广泛应用于分布式系统中,提供高效的远程过程调用能力。本文将深
后端RPC框架微服务ChatBotCourse代码结构解析:深入理解项目架构与模块设计
ChatBotCourse代码结构解析:深入理解项目架构与模块设计 ChatBotCourse是一个完整的聊天机器人学习项目,通过多个版本的逐步演进,帮助开发者
示例工程chrome-cli高级功能:多窗口管理、演示模式与窗口定位终极指南
chrome cli高级功能:多窗口管理、演示模式与窗口定位终极指南 想要通过命令行控制Chrome浏览器,实现自动化工作流和高效的多窗口管理吗?chrome
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考