# 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 ├── ChangeNotifierProvider └── ChangeNotifierProvider ``` ### 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 ← 主题、音效、设置 ChangeNotifierProvider ← 习惯数据、分类 ChangeNotifierProvider ← 导航状态 ] ) ``` ### 6.2 读写模式 ```dart // 读取 — context.watch() 或 Provider.of(context) // UI 组件监听变化并自动重建 final habits = context.watch().activeHabits; // 写入 — context.read() 或 Provider.of(context, listen: false) // 事件处理中调用方法,不触发当前 widget 重建 context.read().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 | 加载指示器 + 错误状态 |