Files
ring-health-docs/superpowers/specs/2026-09-10-ring-health-app-design.md
T

254 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 智能戒指健康 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,免联网下载。~~AGP 8.11.x~~ 已按 R5/R6 改为 **8.13.1**(见下方注记) |
| SDK | `minSdk 26` / `targetSdk 36` / `compileSdk 36`,build-tools **36.1.0** | 对齐厂商 minSdk(26);~~35~~ 已按 R5 改为 **36**,与本机已装 platform/build-tools 一致 |
> **注记(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 关键接口(阶段一定稿)
```kotlin
// 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 开关位)~~、查找戒指(050B)、智能提醒/闹钟占位、**固件升级占位**(OTA 17xx)、佩戴检测~~ | `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,全分支终审后回填)—— 两条与上文不符的实际交付,均为有意为之:**
>
> 1. **设备页删减了四项:** 查找戒指(050B)、智能提醒/闹钟占位、固件升级占位(OTA 17xx)、佩戴检测,
> M1 **未实现**(见 Ruling R27,并已同步删除对应的 8 条字符串资源)。理由是阶段一没有真实协议可对接,
> 摆一排点不动的入口不如不摆。**这些能力仍属于产品范围,将随后续里程碑回归**;本文档上表已用删除线标注。
> 2. **详情页由「四个专属设计」收敛为一个模板** —— 详见**上方「详情子页」表下的注记**。
> 3. **多语言**:`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` |
替换为一次性全局操作,不涉及逻辑改动;执行时按上表逐项核对。