- 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)
483 lines
19 KiB
Markdown
483 lines
19 KiB
Markdown
# 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 | 加载指示器 + 错误状态 |
|