docs: 新增智能戒指健康 App 阶段一设计文档
- 仓库拆分:ring-health-app / ring-health-api(阶段一仅契约) - 技术选型:Kotlin + Compose,工具链钉死本机已装版本 - 架构分层、页面地图、Mock 策略与里程碑 M0-M5 - 厂商 SDK 隔离复用策略(原样 vendor + 自写适配层) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# 智能戒指健康 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.11.x** + JDK 17 + `libs.versions.toml` | 本机已缓存 Gradle 8.14,免联网下载 |
|
||||
| SDK | `minSdk 26` / `targetSdk 35` / `compileSdk 35`,build-tools 35.0.0 | 对齐厂商 minSdk(26);compileSdk 35 与本机已装 platform/build-tools 一致 |
|
||||
|
||||
**阶段一依赖清单(刻意精简)**:`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` | 运动历史列表 |
|
||||
|
||||
**明确不做**(小米有但我们戒指不支持):跑步课程、商城、社区、多设备接入、体重秤/体脂秤。
|
||||
|
||||
## 7. 设计系统与视觉(仿小米运动健康)
|
||||
|
||||
- 浅色为主,白底卡片 + 大圆角 + 轻阴影;大号数字(步数等)用等宽感强的字重。
|
||||
- 指标色板:步数=绿、心率=红/粉、睡眠=蓝/靛、血氧=青、压力=紫、体温=橙(在 `designsystem/Color.kt` 统一定义)。
|
||||
- 通用组件沉淀到 `core/designsystem/component/`:`HealthCard`、`RingProgress`(Canvas 环形进度)、
|
||||
`MetricTile`(图标+数值+单位小卡)、`SectionHeader`、`StatBar`(睡眠分段条/柱状图)。
|
||||
- 深色模式:阶段一**仅保证浅色**可用,深色留到 M5。
|
||||
- 所有中文字符串进 `strings.xml`(默认中文,附英文),支持后续多语言;**禁止硬编码文案**。
|
||||
|
||||
## 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` |
|
||||
|
||||
替换为一次性全局操作,不涉及逻辑改动;执行时按上表逐项核对。
|
||||
Reference in New Issue
Block a user