Files
habo/docs/02-ARCHITECTURE.md
dazhuang aa69f2a91e feat: initial commit - Habo habit tracking app
- Complete MVP with Repository Pattern, SQLite storage
- Provider + ChangeNotifier state management
- Navigation 2.0 with deep link support
- Habit CRUD with twoDayRule, notifications, categories
- Backup/Restore via JSON
- Statistics with streak tracking
- Material You theme support
- Biometric lock support
- Desktop widget support
- 27 languages i18n structure
- Comprehensive test suite (87/89 passing)
2026-04-13 15:02:30 +00:00

483 lines
19 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.
# Habo 架构设计文档
> 基于 REQUIREMENTS.md 中的需求,说明架构决策及其理由
---
## 1. 架构决策总览
### 1.1 为什么选择这个架构
| 决策 | 理由 |
|------|------|
| **纯客户端架构** | 需求零网络依赖、数据不离开设备NFR-安全)。没有服务端意味着无运维成本、无数据泄露风险 |
| **SQLite 本地存储** | 需求持久化存储、离线可用。SQLite 是最成熟的嵌入式数据库,无需额外进程 |
| **Flutter 跨平台** | 需求:支持 Android/iOS/Linux/macOS 四个平台。一套代码覆盖所有目标平台 |
| **Provider + ChangeNotifier** | 需求:响应式 UI 更新。Flutter 官方推荐方案,学习曲线低,适合中等复杂度应用 |
| **Repository Pattern** | 需求:业务逻辑与数据访问解耦。便于替换数据源和编写单元测试 |
| **Navigation 2.0** | 需求深度链接支持habo://settings。声明式路由更易管理页面栈 |
### 1.2 架构约束
- **无网络** — 所有功能离线可用,备份通过文件系统完成
- **单用户** — 无需多用户系统,简化数据模型
- **单数据库** — 一个 SQLite 文件 `habo_db0.db`,数据库版本 9
- **无实时同步** — 数据只存在本地,跨设备迁移依赖手动备份/恢复
---
## 2. 分层架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Presentation Layer (展示层) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Screens │ │ Widgets │ │ Onboarding│ │ Dialogs │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
│ │ │ │ │ │
├───────┴─────────────┴────────────┴──────────────┴─────────────────┤
│ Business Logic Layer (业务逻辑层) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ HabitsManager│ │SettingsManager│ │ Statistics │ │
│ │ (习惯 CRUD) │ │ (设置管理) │ │ (统计计算) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
├─────────┴──────────────────┴────────────────────┴─────────────────┤
│ Service Layer (服务层) │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Notification│ │ Backup │ │UIFeedback│ │ BiometricAuth │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └─────┬──────┘ └─────┬──────┘ └────┬─────┘ └──────┬────────┘ │
│ │ │ │ │ │
├────────┴───────────────┴─────────────┴───────────────┴────────────┤
│ Repository Layer (数据访问层) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ HabitRepo │ │ EventRepo │ │ CategoryRepo │ │
│ │ (接口) │ │ (接口) │ │ (接口) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
├─────────┴───────────────┴───────────────┴─────────────────────────┤
│ Data Layer (数据层) │
│ ┌──────────────────────────────────────────┐ │
│ │ HaboModel → SQLite (sqflite / ffi) │ │
│ │ 数据库: habo_db0.db (版本 9) │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```
---
## 3. 模块职责划分
### 3.1 Presentation Layer
| 模块 | 文件 | 职责 |
|------|------|------|
| **HabitsScreen** | `habits/habits_screen.dart` | 主屏幕入口承载习惯列表、导航栏、FAB |
| **CalendarColumn** | `habits/calendar_column.dart` | 可拖拽排序的习惯列表容器,含分类筛选 |
| **Habit Card** | `habits/habit.dart` | 单个习惯卡片组件(含日历、连续天数、事件标记) |
| **EditHabit** | `habits/edit_habit.dart` | 创建/编辑习惯的表单页面 |
| **StatisticsScreen** | `statistics/statistics_screen.dart` | 统计总览页面 |
| **SettingsScreen** | `settings/settings_screen.dart` | 设置页面 |
| **Onboarding** | `onboarding/onboarding.dart` | 三步引导流程 |
**设计原则**
- 展示层不直接操作数据库,只通过 Manager 类
- 每个 Screen 是独立的 StatefulWidget
- 可复用的 UI 片段提取到 `widgets/` 目录
### 3.2 Business Logic Layer
#### HabitsManager核心管理器
**为什么需要 Manager 而非直接操作 Repository**
```
┌─────────────────────────────────────────────┐
│ HabitsManager │
│ ┌─────────────────────────────────────┐ │
│ │ 1. 内存状态管理 (allHabits 列表) │ │
│ │ 2. 业务规则执行 (排序、归档、undo) │ │
│ │ 3. 服务协调 (通知、备份、小组件) │ │
│ │ 4. 变更通知 (notifyListeners) │ │
│ └─────────────────────────────────────┘ │
│ │ │ │ │
│ HabitRepo EventRepo CategoryRepo │
│ │ │ │ │
│ NotificationService BackupService ... │
└─────────────────────────────────────────────┘
```
**职责清单**
- 维护 `allHabits` 内存列表(活跃 + 归档)
- 协调 Repository 的 CRUD 操作
- 调用通知服务更新提醒
- 调用小组件服务更新桌面小组件
- 调用 UI 反馈服务展示消息
- 处理 undo删除后可撤销
- 处理拖拽排序的位置更新
#### SettingsManager
**职责**
- 管理所有用户偏好设置(主题、音效、通知等)
- 使用 SharedPreferences 持久化
- 管理 SoLoud 音效引擎的初始化和播放
- 通知 UI 主题变化
#### Statistics
**为什么统计是纯函数而非 Manager**
统计计算是无状态的——每次从当前习惯数据实时计算,不需要维护额外状态。因此设计为纯静态方法 `Statistics.calculateStatistics()`
### 3.3 Service Layer
**为什么需要 Service 层?**
跨领域的关注点通知、备份、UI反馈、认证不属于任何单一 Manager 的职责,提取为独立服务便于复用和测试。
| 服务 | 依赖 | 被谁调用 |
|------|------|----------|
| NotificationService | awesome_notifications | HabitsManager |
| BackupService | RepositoryFactory, 文件系统 | HabitsManager, SettingsScreen |
| UIFeedbackService | ScaffoldMessenger | HabitsManager |
| BiometricAuthService | local_auth | BiometricAuthWrapper |
| HomeWidgetService | home_widget | HabitsManager |
| ServiceLocator | 所有服务 | main.dart (初始化) |
### 3.4 Repository Layer
**为什么用 Repository Pattern 而非直接用 HaboModel**
```
// 不好的方式 — UI 直接依赖数据层
HaboModel().insertHabit(habit);
// 好的方式 — 通过抽象接口解耦
HabitRepository habitRepo = RepositoryFactory.createHabitRepository(model);
habitRepo.createHabit(habit);
```
好处:
1. **可测试** — 可以用 MockRepository 替换真实数据库
2. **可替换** — 未来换数据库引擎只需改 Repository 实现
3. **关注点分离** — UI/Manager 不知道也不关心数据如何存储
### 3.5 Data Layer (HaboModel)
**定位**:直接操作 SQLite 的底层类,是整个数据层的基石。
**特殊处理**
- 移动端使用 `sqflite`
- 桌面端Linux/macOS使用 `sqflite_common_ffi`
- 启用 `PRAGMA foreign_keys = ON` 确保级联删除
- 管理数据库版本升级迁移(版本 1→9
---
## 4. 数据流设计
### 4.1 用户打卡事件流
```
用户点击日历日期
├─── 一键打卡模式 ON
│ └── 直接切换 check/clear
└─── 一键打卡模式 OFF
└── 弹出选择菜单
├── Check (完成)
├── Progress (进度,数值型)
├── Fail (失败)
├── Skip (跳过)
├── Note (备注)
└── Date (修改日期)
HabitsManager.addEvent(id, date, eventData)
┌─────────┴─────────┐
│ │
内存状态更新 Repository 写入
habitData.events EventRepository.add()
│ │
│ SQLite INSERT/REPLACE
│ │
▼ ▼
notifyListeners() NotificationService
│ (奖励/惩罚通知)
Provider → Widget 重建
(日历标记更新、连续天数更新)
```
### 4.2 应用初始化流
```
main()
├── 1. WidgetsFlutterBinding.ensureInitialized()
├── 2. SettingsManager.loadData() ← SharedPreferences
├── 3. HaboModel.initDatabase() ← SQLite 初始化
├── 4. ServiceLocator.init(model) ← 注册所有服务
├── 5. HabitsManager(repos, services) ← 注入依赖
├── 6. HabitsManager.loadHabits() ← 从数据库加载所有习惯
├── 7. NotificationService 初始化
├── 8. AppRouter(stateManagers) ← 创建路由
├── 9. 日变化定时器启动 ← 检测跨日刷新
└── runApp(MultiProvider → MaterialApp.router)
├── ChangeNotifierProvider<SettingsManager>
├── ChangeNotifierProvider<HabitsManager>
└── ChangeNotifierProvider<AppStateManager>
```
### 4.3 跨日刷新流
```
DayChangeTimer (每小时检查)
├── 检测到日期变化 (now.day != lastDay.day)
│ │
│ ├── HabitsManager.loadHabits() ← 重新加载习惯数据
│ ├── NotificationService.reset() ← 重置通知调度
│ └── HomeWidgetService.update() ← 更新桌面小组件
└── 未变化 → 等待下次检查
```
---
## 5. 导航架构
### 5.1 为什么选择 Navigation 2.0
| Navigation 1.0 (Navigator) | Navigation 2.0 (RouterDelegate) |
|---------------------------|----------------------------------|
| 命令式 push/pop | 声明式页面栈 |
| 不支持深度链接 | 原生支持 URL 映射 |
| 难以管理复杂页面栈 | 通过状态管理器统一控制 |
### 5.2 页面栈结构
```
AppRouter (RouterDelegate)
├── SplashScreen ← 初始页(条件判断后自动跳转)
│ │
│ ├── 首次使用 → OnboardingScreen
│ ├── 有新版本 → WhatsNewScreen
│ └── 正常使用 → HabitsScreen (主页面)
├── HabitsScreen ← 主页面(始终在栈底)
│ ├── → StatisticsScreen
│ ├── → SettingsScreen
│ ├── → CreateHabitScreen
│ └── → EditHabitScreen
└── 深度链接: habo://settings → 直接打开 SettingsScreen
```
### 5.3 状态驱动导航
```
AppStateManager (ChangeNotifier)
├── _statistics: bool → 控制 StatisticsScreen 显示
├── _settings: bool → 控制 SettingsScreen 显示
├── _onboarding: bool → 控制 OnboardingScreen 显示
├── _whatsNew: bool → 控制 WhatsNewScreen 显示
├── _createHabit: bool → 控制 CreateHabitScreen 显示
└── _editHabit: bool → 控制 EditHabitScreen 显示
AppRouter 监听 AppStateManager 变更
└── 根据 bool 标志位组合构建 pages 列表
```
**关键设计决策**`AppRouter` 不监听 `HabitsManager`,因为习惯数据变化不应触发导航跳转,只应刷新当前页面内容。
---
## 6. 状态管理策略
### 6.1 Provider 分布
```
MultiProvider(
providers: [
ChangeNotifierProvider<SettingsManager> ← 主题、音效、设置
ChangeNotifierProvider<HabitsManager> ← 习惯数据、分类
ChangeNotifierProvider<AppStateManager> ← 导航状态
]
)
```
### 6.2 读写模式
```dart
// 读取 — context.watch<T>() 或 Provider.of<T>(context)
// UI 组件监听变化并自动重建
final habits = context.watch<HabitsManager>().activeHabits;
// 写入 — context.read<T>() 或 Provider.of<T>(context, listen: false)
// 事件处理中调用方法,不触发当前 widget 重建
context.read<HabitsManager>().addEvent(id, date, event);
```
### 6.3 状态生命周期
| 状态 | 作用域 | 生命周期 |
|------|--------|----------|
| HabitsManager.allHabits | 全局 | 应用启动到关闭 |
| SettingsManager.* | 全局 | 应用启动到关闭 |
| AppStateManager.* | 全局 | 应用启动到关闭 |
| HabitData.events | 每个习惯 | 随 HabitsManager 加载 |
| 习惯卡片 UI 状态 (streak, calendar) | 单个 Widget | Widget 生命周期 |
---
## 7. 主题系统设计
### 7.1 为什么支持 5 种主题模式
| 主题 | 目标用户 | 技术实现 |
|------|----------|----------|
| Device | 大多数用户 | 跟随系统 MediaQuery |
| Light | 强制浅色偏好 | 固定浅色 ColorScheme |
| Dark | 强制深色偏好 | 固定深色 ColorScheme |
| OLED | OLED 屏幕用户 | 纯黑 (#000000) 背景 |
| Material You | Android 12+ 用户 | dynamic_color 提取壁纸颜色 |
### 7.2 颜色体系
```
核心颜色常量:
primary: #09BF30 (完成/主色)
red: #F44336 (失败)
skip: #FBC02D (跳过)
orange: #FF9800 (两天法则警告)
progress: #2196F3 (进度)
progressBg: #E3F2FD (进度背景)
用户可自定义:
checkColor: 默认 primary
failColor: 默认 red
skipColor: 默认 skip
progressColor: 默认 progress
```
---
## 8. 通知系统设计
### 8.1 通知类型
| 类型 | 触发条件 | 内容 |
|------|----------|------|
| **每日提醒** | 用户设定时间 | "Do not forget to check your habits." |
| **奖励通知** | 习惯标记完成 + showReward 开启 | "Congratulations! Your reward: {reward}" |
| **惩罚通知** | 习惯标记失败 + showSanction 开启 | "Oh no! Your sanction: {sanction}" |
### 8.2 通知调度策略
```
创建/编辑习惯
└── NotificationService.resetNotifications()
├── 取消所有现有通知
└── 为每个启用通知的习惯创建定时通知
└── awesome_notifications.createNotification()
├── channel: "habit_notifications"
├── schedule: 每日重复 at notTime
└── payload: habitId
删除习惯
└── NotificationService.removeNotifications(id)
```
---
## 9. 备份系统设计
### 9.1 为什么选择 JSON 文件而非二进制
- **用户可读** — 用户可以打开 JSON 查看自己的数据
- **调试友好** — 开发时可直接检查备份内容
- **版本控制** — 可以 git diff 对比变化
- **跨平台** — JSON 在所有平台上通用
### 9.2 备份文件格式
```json
{
"version": 3,
"habits": [...],
"events": { "habitId": { "date": [dayType, ...] } },
"categories": [...],
"habit_categories": [{ "habit_id": 1, "category_id": 1 }],
"metadata": {
"imported_from": "legacy_list",
"import_timestamp": "ISO8601"
}
}
```
### 9.3 兼容性策略
- 新格式包含 `version` 字段用于版本识别
- 支持读取旧版数组格式(无 version 字段 = 旧版)
- 导入时自动转换为当前格式
- 文件大小限制 10MB
---
## 10. 依赖注入设计
### 10.1 ServiceLocator 模式
```
ServiceLocator (单例)
├── 提供:
│ ├── RepositoryFactory → 创建各 Repository
│ ├── NotificationService
│ ├── BackupService
│ ├── UIFeedbackService
│ ├── BiometricAuthService
│ └── HomeWidgetService
├── 初始化时接收:
│ └── HaboModel (共享数据库连接)
└── 使用方:
└── main.dart 中创建并注入到 HabitsManager
```
### 10.2 HabitsManager 的依赖注入
```dart
HabitsManager(
habitRepository: repoFactory.habitRepository,
eventRepository: repoFactory.eventRepository,
categoryRepository: repoFactory.categoryRepository,
backupService: serviceLocator.backupService, // 可选
notificationService: serviceLocator.notificationService, // 可选
uiFeedbackService: serviceLocator.uiFeedbackService, // 可选
)
```
可选服务的设计使得在测试时可以传入 null方便隔离测试业务逻辑。
---
## 11. 错误处理策略
| 层级 | 策略 | 用户体验 |
|------|------|----------|
| Data Layer (HaboModel) | try-catch + debugPrint | 静默失败,不中断应用 |
| Repository Layer | 抛出异常 | 向上传播 |
| Service Layer | 返回结果对象 (BackupResult) | UI 反馈服务展示错误消息 |
| Manager Layer | catch + UIFeedbackService.showError() | 用户看到错误提示 |
| Presentation Layer | FutureBuilder 处理 loading/error | 加载指示器 + 错误状态 |