16 KiB
智能戒指健康 App 设计文档(阶段一)
- 日期:2026-09-10
- 状态:待评审
- 产品代号(占位):
ring-health - 相关仓库:
ring-health-app(Android App)、ring-health-api(后端 API)
1. 背景与目标
我们要为自有的智能戒指/手环做一款手机 App,用于后续与设备联调、并最终面向用户。 交互参照小米运动健康(Mi Fitness),但按我们戒指的真实能力裁剪。
阶段一目标(本文档范围):用原生 Android + Jetpack Compose 搭出完整可运行的 静态页面骨架,页面齐全、导航顺畅、真机可跑;所有数据来自本地 Mock。
不在阶段一范围:真实蓝牙连接、协议解析、本地数据库、账号登录、云端同步。 这些在 M2 及以后的里程碑中,通过预留的接口边界接入。
2. 现状:厂商 SDK 分析(已探明)
手上的 SmartRingF01_SDK 不是 .aar 库,而是厂商「鑫域隆 SmartRingF01/F02」的完整 Demo App。
| 项 | 现状 |
|---|---|
| 工程名 / 包名 | SmartRingDCLT_SDK / com.sr.smartring |
| 可复用核心 | BluetoothLeClass(BLE 扫描/连接/GATT 收发,仅依赖 Android 框架,零第三方依赖) |
| 协议收发 | 写在厂商 MainActivity 的静态方法:sendBleData / recvBleData / recvBleWriteData,帧格式 DC 0001 <cmd> <data> LT(下行)、DC 0002 …(上报) |
| 辅助类 | HEROUtils(OTA 下载 + CRC32)、Utils、BluetoothApplication、LeDeviceListAdapter |
| 技术栈 | Java + Android Support Library(非 AndroidX) + EventBus 3 + Gradle 6.7.1 / AGP 4.2.2(很旧) |
| 协议文档 | 鑫域隆智能戒指协议.txt(GBK 编码,含全部指令码) |
戒指能力(来自协议文档,决定我们能做哪些页面): 计步、心率、血氧、睡眠、血压、体温、压力、运动检测、查找戒指、电量、佩戴/充电状态、OTA 升级、自动监测开关配置、时间同步。
复用策略:SDK 内容不修改。M2 接 BLE 时,把 BluetoothLeClass 等纯通讯类原样拷贝进
App 的 vendor/ 包(保持 com.sr.smartring 包名与代码原样),我们只新写一层适配器去调用它。
UI 永远不直接依赖厂商代码。
3. 仓库与目录结构
遵循团队 docs/ + repos/ 惯例;仓库名 kebab-case,移动端 -app、后端 -api。
d:\cherry
├── SmartRingF01_SDK/ # 厂商原始工程(只读参考,不纳入任何 git 仓库)
├── docs/ # 项目文档根(独立 git 仓库)
│ └── superpowers/specs/ # 设计/技术文档
└── repos/
├── ring-health-app/ # Android App(Kotlin + Compose) ← 阶段一主战场
└── ring-health-api/ # 后端 API ← 阶段一仅脚手架 + 接口契约
ring-health-app:阶段一全部实现在此。ring-health-api:阶段一只放空骨架(README + gitignore)与docs/api-contract.md契约草稿, 不写业务接口。后端技术栈(Spring Boot / .NET)待 M4 真正做云同步时再定。
4. App 技术选型
| 项 | 选择 | 理由 |
|---|---|---|
| 语言 | Kotlin | Compose 唯一舒适选择 |
| UI | Jetpack Compose + Material 3 | 写卡片/图表类仿小米页面效率最高 |
| 导航 | Navigation-Compose,单 Activity | 多 Tab + 深层子页标准做法 |
| 架构 | MVVM + StateFlow(UiState) |
官方推荐,易测试 |
| 异步 | Coroutines + Flow | — |
| 图表 | Compose Canvas 手绘(今日大环、柱状/折线占位) | 阶段一不引第三方图表库(YAGNI) |
| DI | 手动组合根(Application 中构造仓库) |
阶段一无需 Hilt,后续可平滑替换 |
| 构建 | Gradle 8.14 + AGP 8.13.1 + JDK 17 + libs.versions.toml |
本机已缓存 Gradle 8.14,免联网下载。 |
| SDK | minSdk 26 / targetSdk 36 / compileSdk 36,build-tools 36.1.0 |
对齐厂商 minSdk(26); |
注记(2026-09-10,M1 实施后回填): 上表两行原写 AGP 8.11.x / compileSdk 35 / build-tools 35.0.0, 与最终交付的
app/build.gradle.kts不符 —— 代码是对的,是本文档当时没跟着更新。 实际工具链:AGP 8.13.1、compileSdk/targetSdk 36、buildToolsVersion 36.1.0、minSdk 26、 Kotlin 2.2.20。下调理由见 Ruling R5(工具链以本机已装版本为准)与 R6(越界依赖下调至工具链可接受的最新版本), 完整证据链写在gradle/libs.versions.toml顶部的注释里。 另外compose-bom用的是 2026.06.01(compose 1.11.4),不是 2026.09.00 —— 那组版本要 compileSdk 37 + AGP 9.1.0,与本工具链互斥。
阶段一依赖清单(刻意精简):androidx.core:core-ktx、androidx.activity:activity-compose、
androidx.lifecycle:lifecycle-runtime-compose / lifecycle-viewmodel-compose、
androidx.navigation:navigation-compose、androidx.compose:compose-bom(ui / material3 /
material-icons-extended / ui-tooling-preview)、kotlinx-coroutines。
不引 Hilt、Room、Retrofit、第三方图表库。
5. App 架构分层(单 :app 模块 + 严格分包)
com.placeholder.ringhealth
├── RingHealthApplication.kt # 组合根:手动构造 Repository,注入 ViewModel
├── MainActivity.kt # 唯一 Activity,承载 Compose NavHost
├── core/
│ ├── designsystem/ # Theme/Color/Type/Shape + 通用组件
│ │ └── component/ # HealthCard、RingProgress、MetricTile、SectionHeader…
│ ├── common/ # 格式化与扩展(步数、时长、心率区间)
│ └── navigation/ # Route 常量与 NavGraph
├── domain/
│ ├── model/ # 纯 Kotlin 数据模型(无 Android 依赖)
│ └── repository/ # 仓库接口(UI 只依赖这里)
├── data/
│ └── mock/ # Mock 实现 + 造数器(阶段一)
│ # M2 将新增 data/sdk/ 作为厂商 SDK 适配层
├── feature/ # 每个 Tab / 子页一个包:Screen + ViewModel + UiState
│ ├── home/ workout/ device/ profile/ detail/
└── vendor/ # 预留:M2 原样存放厂商 com.sr.smartring 通讯类(不改一行)
核心边界原则:UI(feature/*)只依赖 domain/repository 的接口与 domain/model。
阶段一注入 Mock 实现;M2 新增 SDK 适配实现,接口不变则 UI 零改动。
5.1 关键接口(阶段一定稿)
// domain/repository
interface RingDeviceRepository {
val deviceState: StateFlow<RingDeviceState> // 连接态/电量/型号/固件版本
suspend fun refreshDeviceState()
}
interface HealthDataRepository {
fun observeTodaySummary(): Flow<HealthSummary>
fun observeMetricHistory(metric: MetricType, range: DateRange): Flow<List<MetricPoint>>
fun observeWorkouts(): Flow<List<WorkoutRecord>>
}
// domain/model(节选)
enum class MetricType { STEP, HEART_RATE, SLEEP, BLOOD_OXYGEN, STRESS, TEMPERATURE, BLOOD_PRESSURE }
data class RingDeviceState(
val connected: Boolean, val name: String, val batteryPercent: Int,
val firmwareVersion: String, val charging: Boolean,
)
data class HealthSummary( // 今日概览
val steps: Int, val stepGoal: Int, val calories: Int, val distanceKm: Double,
val heartRate: Int?, val sleepMinutes: Int?, val bloodOxygen: Int?,
val stress: Int?, val temperature: Double?, val bloodPressure: String?, // 血压未开启时为 null
)
data class MetricPoint(val timestamp: Long, val value: Double)
约定:ViewModel 用 stateIn 把 Flow 转成 StateFlow<XxxUiState>;null 表示"该指标未开启/无数据",
UI 统一走"未开启占位态",不显示 0。
6. 页面地图与路由
底部四 Tab(仿小米运动健康),并按戒指能力裁剪:
| Tab | 页面内容 | 路由 |
|---|---|---|
| ① 健康·首页 | 顶部问候 + 设备连接状态条;今日大环(步数/卡路里/距离);指标卡片网格:心率 / 睡眠 / 血氧 / 压力 / 体温(血压显示"未开启/需校准"占位) | home |
| ② 运动 | "开始运动"入口(跑步/骑行/自由训练占位)+ 运动历史记录列表(静态) | workout |
| ③ 设备 | 戒指卡片(名称/电量/连接态/固件)+ 设置列表:健康自动监测开关(心率/血氧/血压/压力/睡眠,对应协议 0502 开关位) |
device |
| ④ 我的 | 头像/昵称(登录占位)、App 设置(单位/通知)、通知权限引导、关于/检查更新、帮助与反馈 | profile |
详情子页(全部静态 + Canvas 图表占位):
| 路由 | 内容 |
|---|---|
detail/step |
今日步数详情:大环 + 7 天/月切换柱状图 + 说明文案 |
detail/heart_rate |
心率详情:当日折线/区间统计(静息/运动)+ 历史区间 |
detail/sleep |
睡眠详情:深睡/浅睡/清醒分段条 + 入睡/起床时间 |
detail/blood_oxygen |
血氧详情:趋势折线 + 正常范围说明 |
device/settings |
戒指功能开关详情(静态开关,M2 后写向设备) |
workout/history |
运动历史列表 |
M1 实际交付与上表的差异(2026-09-10 回填,全分支终审发现): 上面
detail/*的四条路由实际合并成了一条:detail/{metric}(Routes.DETAIL_PATTERN, 参数为MetricType.name),七个指标共用同一个模板 —— 柱状趋势图 + 周/月切换 + 平均/最高/最低。 仅睡眠保留了专属区块(SleepStagesCard:深睡/浅睡/清醒分段条 + 入睡/起床时间)。 未实现的是: 步数的大环、心率的折线/静息-运动区间、血氧的正常范围说明文案。 收敛本身是有意的(避免七套几乎相同的页面代码),但当时没留下裁定记录 —— 这不妥, 此处补记,供后续决定是否回归为专属设计。这是 M1 已知的、有意的范围收窄,不是漏做。
明确不做(小米有但我们戒指不支持):跑步课程、商城、社区、多设备接入、体重秤/体脂秤。
7. 设计系统与视觉(仿小米运动健康)
- 浅色为主,白底卡片 + 大圆角 + 轻阴影;大号数字(步数等)用等宽感强的字重。
- 指标色板:步数=绿、心率=红/粉、睡眠=蓝/靛、血氧=青、压力=紫、体温=橙(在
designsystem/Color.kt统一定义)。 - 通用组件沉淀到
core/designsystem/component/:HealthCard、RingProgress(Canvas 环形进度)、MetricTile(图标+数值+单位小卡)、SectionHeader、StatBar(睡眠分段条/柱状图)。 - 深色模式:阶段一仅保证浅色可用,深色留到 M5。
- 所有中文字符串进
strings.xml(默认中文,附英文),支持后续多语言;禁止硬编码文案。
M1 实施状态注记(2026-09-10,全分支终审后回填)—— 两条与上文不符的实际交付,均为有意为之:
- 设备页删减了四项: 查找戒指(050B)、智能提醒/闹钟占位、固件升级占位(OTA 17xx)、佩戴检测, M1 未实现(见 Ruling R27,并已同步删除对应的 8 条字符串资源)。理由是阶段一没有真实协议可对接, 摆一排点不动的入口不如不摆。这些能力仍属于产品范围,将随后续里程碑回归;本文档上表已用删除线标注。
- 详情页由「四个专属设计」收敛为一个模板 —— 详见上方「详情子页」表下的注记。
- 多语言:
strings.xml目前只有中文,没有values-en/。本文档 「附英文」与 §198 把多语言排在 M5 两处自相矛盾;以 M5 为准,M1 不做。
8. Mock 数据策略
data/mock/MockDataProvider.kt提供确定性演示数据(固定随机种子),保证每次运行截图一致、便于展示。- Mock 仓库模拟轻微异步延迟(~200ms),让加载态/占位骨架在阶段一就能跑通。
- 开关类设置的状态用内存态(
StateFlow)保存,重启重置;阶段一不引入持久化。
9. API 仓库(阶段一)
ring-health-api 阶段一只包含:
ring-health-api/
├── README.md # 说明:后端栈待定,M4 落地
├── .gitignore
└── docs/api-contract.md # 接口契约草稿(登录、设备绑定、健康数据同步的字段雏形)
契约草稿仅为对齐未来前后端边界,不实现。后端技术栈(Spring Boot / .NET)在 M4 前单独定。
10. 里程碑与验收
| 阶段 | 内容 | 验收标准 |
|---|---|---|
| M0 基建 | 建 docs/、repos/ 目录;初始化两个 git 仓库;gitignore/README |
两仓库结构就绪,符合命名规范 |
| M1 静态 App(阶段一) | Compose 工程骨架、主题、四 Tab 导航、本文档第 6 节全部页面、Mock 仓库 | 真机/模拟器可跑;四 Tab 与全部子页可切换;无崩溃;assembleDebug 通过 |
| M2 SDK 接入 | vendor/ 原样放厂商通讯类;新增 data/sdk/ 适配层实现既有接口;BLE 权限、扫描、连接 |
真机扫描到戒指、连接成功、读到电量/固件 |
| M3 数据打通 | 协议解析(帧 DC…LT)、Mock→真数据、ROOM 本地存储、真实图表 |
首页与详情显示真机数据 |
| M4 云端/账号 | 定后端栈、实现登录与同步接口、App 接入 | 接口联调通过 |
| M5 发布 | 签名、图标、深色模式、多语言、内测包 | 可分发 APK |
本次实施范围 = M0 + M1。
11. 风险与决策记录
| 风险/决策 | 说明与对策 |
|---|---|
| 厂商 SDK 老旧(Support Library / EventBus)与 Compose 工程共存 | BluetoothLeClass 零第三方依赖,风险低;M2 隔离在 vendor/ 包内,仅当必需才考虑 jetifier |
| 占位命名需全局替换 | 替换清单见 §12;集中一处记录,替换时按清单执行 |
| BLE 权限(Android 12+) | M2 统一处理 BLUETOOTH_SCAN/CONNECT + 位置权限与引导 UI,阶段一不涉及 |
| 手绘 Canvas 图表工作量 | 阶段一只做环形 + 简单柱/折线,避免过度实现;复杂图表留 M3 |
| 后端栈未定 | 有意延迟到 M4,避免两仓库空转 |
| 本机构建环境版本 | 已探明:JDK 17、Android SDK(platform 34/35/36)、build-tools 仅 35.0.0 / 36.1.0、Gradle 缓存 8.12/8.14、无全局 gradle。故工具链钉死在已安装版本,避免构建时联网拉取缺失组件 |
已确认决策:① 前后端分离,两个仓库;② Jetpack Compose;③ 阶段一单 :app 分层模块;
④ 四个 Tab 与全部详情子页都做;⑤ 后端阶段一只留契约;⑥ 占位命名后续统一替换。
12. 占位命名与替换清单
待正式产品名/域名确定后,需统一替换:
| 位置 | 占位值 |
|---|---|
| 仓库名 | ring-health-app、ring-health-api(含 git 服务器上名称) |
applicationId / namespace |
com.placeholder.ringhealth |
| 包路径 | com/placeholder/ringhealth 目录树 |
app_name(strings.xml) |
RingHealth(占位) |
| 文档与 README 中的产品代号 | ring-health |
替换为一次性全局操作,不涉及逻辑改动;执行时按上表逐项核对。