- 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)
19 KiB
19 KiB
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);
好处:
- 可测试 — 可以用 MockRepository 替换真实数据库
- 可替换 — 未来换数据库引擎只需改 Repository 实现
- 关注点分离 — 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 读写模式
// 读取 — 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 备份文件格式
{
"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 的依赖注入
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 | 加载指示器 + 错误状态 |