本次提交涵盖多项功能优化与重构: 1. 重构pro_image_editor依赖为官方托管版本,移除本地包引用 2. 拆分角色表情枚举至独立文件,优化代码复用性 3. 新增壁纸收藏、预加载、健康检测服务与本地存储支持 4. 完善API响应类型安全检查与排行榜服务能力 5. 新增应用锁设置路由与页面支持 6. 优化路由跳转使用常量路径替代硬编码字符串 7. 新增阅读报告分享功能与设置变更日志服务 8. 修复多处类型转换与空指针风险问题 9. 调整API超时时间优化网络请求表现 10. 统一文件头格式与部分UI组件样式
1149 lines
50 KiB
Markdown
1149 lines
50 KiB
Markdown
# 闲言APP 架构重构规格书
|
||
|
||
> 创建时间: 2026-05-24
|
||
> 版本: v1.0
|
||
> 作用: 描述当前架构三大核心问题及其解决方案,供 AI 开发者参考实施
|
||
> 适用范围: `lib/core/` 和 `lib/features/` 全部模块
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [一、总览](#一总览)
|
||
- [二、问题1: 状态管理碎片化](#二问题1-状态管理碎片化)
|
||
- [2.1 现状分析](#21-现状分析)
|
||
- [2.2 目标架构](#22-目标架构)
|
||
- [2.3 迁移映射表](#23-迁移映射表)
|
||
- [2.4 迁移步骤](#24-迁移步骤)
|
||
- [2.5 受影响文件清单](#25-受影响文件清单)
|
||
- [2.6 风险评估](#26-风险评估)
|
||
- [三、问题2: 服务层全是静态方法](#三问题2-服务层全是静态方法)
|
||
- [3.1 现状分析](#31-现状分析)
|
||
- [3.2 目标架构](#32-目标架构)
|
||
- [3.3 迁移步骤](#33-迁移步骤)
|
||
- [3.4 代码示例](#34-代码示例)
|
||
- [3.5 受影响文件清单](#35-受影响文件清单)
|
||
- [3.6 风险评估](#36-风险评估)
|
||
- [四、问题3: Provider 与 Service 职责不清](#四问题3-provider-与-service-职责不清)
|
||
- [4.1 现状分析](#41-现状分析)
|
||
- [4.2 目标架构](#42-目标架构)
|
||
- [4.3 迁移步骤](#43-迁移步骤)
|
||
- [4.4 代码示例](#44-代码示例)
|
||
- [4.5 受影响文件清单](#45-受影响文件清单)
|
||
- [4.6 风险评估](#46-风险评估)
|
||
- [五、实施优先级与时间线](#五实施优先级与时间线)
|
||
- [六、通用迁移检查清单](#六通用迁移检查清单)
|
||
|
||
---
|
||
|
||
## 一、总览
|
||
|
||
当前项目存在三大架构级问题,彼此关联、相互影响:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 架构问题关系图 │
|
||
│ │
|
||
│ 问题1: 存储碎片化 ──────→ 问题2: 服务静态化 ──────→ 问题3: 职责不清 │
|
||
│ (5种存储方案) (无法注入/测试) (3种通知模式混用) │
|
||
│ │
|
||
│ 解决: 统一3层存储 解决: Riverpod Provider 解决: 分层架构 │
|
||
│ KvStorage/Secure/DB Service→Provider模式 UI/State/Service │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**推荐实施顺序**: 问题1 → 问题2 → 问题3
|
||
|
||
原因:存储层是基础设施,服务层依赖存储层,状态层依赖服务层。自底向上重构,避免循环依赖。
|
||
|
||
---
|
||
|
||
## 二、问题1: 状态管理碎片化
|
||
|
||
### 2.1 现状分析
|
||
|
||
项目同时存在 **5 种存储方案**,功能高度重叠:
|
||
|
||
| 存储方案 | 底层实现 | 文件路径 | 用途 |
|
||
|---------|---------|---------|------|
|
||
| `KvStorage` | SharedPreferences | `lib/core/storage/kv_storage.dart` | 通用KV(主题/语言/引导页等) |
|
||
| `AppKVStore` | Hive | `lib/core/storage/app_kv_store.dart` | 通用KV(搜索历史/工具统计/用户偏好等) |
|
||
| `SecureStorage` | flutter_secure_storage | `lib/core/storage/secure_storage.dart` | 敏感数据(Token/密码) |
|
||
| `Hive` 直接使用 | Hive box | 散布于各 service | 部分服务直接操作 Hive box |
|
||
| `SharedPreferences` 直接使用 | SharedPreferences | 散布于各 service | 部分服务直接获取 SP 实例 |
|
||
|
||
**核心冲突**: `KvStorage` 和 `AppKVStore` 功能高度重叠:
|
||
|
||
```dart
|
||
// KvStorage (基于 SharedPreferences)
|
||
KvStorage.getString(key)
|
||
KvStorage.setString(key, value)
|
||
KvStorage.getBool(key)
|
||
KvStorage.setBool(key, value)
|
||
KvStorage.getInt(key)
|
||
KvStorage.setInt(key, value)
|
||
|
||
// AppKVStore (基于 Hive) — 方法签名几乎完全一致
|
||
AppKVStore.getString(key)
|
||
AppKVStore.setString(key, value)
|
||
AppKVStore.getBool(key)
|
||
AppKVStore.setBool(key, value)
|
||
AppKVStore.getInt(key)
|
||
AppKVStore.setInt(key, value)
|
||
```
|
||
|
||
**问题影响**:
|
||
- 新开发者不知道该用哪个,随意选择导致数据分散
|
||
- `AppKVStore` 内部已有 `_migrateFromSharedPreferences()` 迁移逻辑,说明历史遗留问题
|
||
- 同一功能的数据可能分散在两个存储中,查询时需要检查两处
|
||
- `SecureStorage` 在 macOS 上降级为 SharedPreferences,与 `KvStorage` 底层冲突
|
||
|
||
### 2.2 目标架构
|
||
|
||
统一为 **3 层存储架构**,每层职责明确、互不重叠:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ 存储架构 (3层) │
|
||
├──────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Layer 1: KvStorage (通用KV) │
|
||
│ ┌────────────────────────────────────────────┐ │
|
||
│ │ 底层: Hive │ │
|
||
│ │ 用途: 主题/语言/设置/偏好/缓存/统计 │ │
|
||
│ │ 特点: 高性能、支持多Box、同步读取 │ │
|
||
│ │ 替换: AppKVStore + KvStorage(旧SP) → 统一 │ │
|
||
│ └────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Layer 2: SecureStorage (敏感数据) │
|
||
│ ┌────────────────────────────────────────────┐ │
|
||
│ │ 底层: flutter_secure_storage │ │
|
||
│ │ 用途: Token/密码/密钥/隐私数据 │ │
|
||
│ │ 特点: iOS Keychain / Android EncryptedSP │ │
|
||
│ │ 保持: 现有实现不变 │ │
|
||
│ └────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Layer 3: Database (结构化数据) │
|
||
│ ┌────────────────────────────────────────────┐ │
|
||
│ │ 底层: Drift (SQLite) │ │
|
||
│ │ 用途: 聊天记录/收藏列表/离线队列 │ │
|
||
│ │ 特点: 关系查询、事务、复杂条件筛选 │ │
|
||
│ │ 状态: 待建设(当前用Hive box替代) │ │
|
||
│ └────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**关键决策**: 统一 `KvStorage` 底层从 SharedPreferences 迁移到 Hive
|
||
|
||
理由:
|
||
1. Hive 性能优于 SharedPreferences(内存级读取 vs 磁盘IO)
|
||
2. Hive 支持同步读取,SharedPreferences 只有异步
|
||
3. `AppKVStore` 已有 SP→Hive 迁移逻辑,可复用
|
||
4. Hive 支持多 Box 分区,比 SP 的扁平命名空间更清晰
|
||
|
||
### 2.3 迁移映射表
|
||
|
||
#### `AppKVStore` → `KvStorage` 方法映射
|
||
|
||
| AppKVStore 方法 | KvStorage 新方法 | 说明 |
|
||
|----------------|-----------------|------|
|
||
| `AppKVStore.getString(key)` | `KvStorage.getString(key)` | 签名一致 |
|
||
| `AppKVStore.setString(key, value)` | `KvStorage.setString(key, value)` | 返回值从 `Future<void>` 改为 `Future<bool>` |
|
||
| `AppKVStore.getInt(key)` | `KvStorage.getInt(key)` | 签名一致 |
|
||
| `AppKVStore.setInt(key, value)` | `KvStorage.setInt(key, value)` | 返回值从 `Future<void>` 改为 `Future<bool>` |
|
||
| `AppKVStore.getBool(key)` | `KvStorage.getBool(key)` | 签名一致 |
|
||
| `AppKVStore.setBool(key, value)` | `KvStorage.setBool(key, value)` | 返回值从 `Future<void>` 改为 `Future<bool>` |
|
||
| `AppKVStore.getDouble(key)` | `KvStorage.getDouble(key)` | 需新增 |
|
||
| `AppKVStore.setDouble(key, value)` | `KvStorage.setDouble(key, value)` | 需新增 |
|
||
| `AppKVStore.getStringList(key)` | `KvStorage.getStringList(key)` | 签名一致 |
|
||
| `AppKVStore.setStringList(key, value)` | `KvStorage.setStringList(key, value)` | 签名一致 |
|
||
| `AppKVStore.remove(key)` | `KvStorage.remove(key)` | 签名一致 |
|
||
| `AppKVStore.containsKey(key)` | `KvStorage.containsKey(key)` | 签名一致 |
|
||
| `AppKVStore.getSearchHistory()` | `KvStorage.getSearchHistory()` | 需新增便捷方法 |
|
||
| `AppKVStore.addSearchHistory(keyword)` | `KvStorage.addSearchHistory(keyword)` | 需新增便捷方法 |
|
||
| `AppKVStore.clearSearchHistory()` | `KvStorage.clearSearchHistory()` | 需新增便捷方法 |
|
||
| `AppKVStore.getToolUsageStats()` | `KvStorage.getToolUsageStats()` | 需新增便捷方法 |
|
||
| `AppKVStore.incrementToolUsage(id, name)` | `KvStorage.incrementToolUsage(id, name)` | 需新增便捷方法 |
|
||
| `AppKVStore.getUserPref(key)` | `KvStorage.getUserPref(key)` | 需新增便捷方法 |
|
||
| `AppKVStore.setUserPref(key, value)` | `KvStorage.setUserPref(key, value)` | 需新增便捷方法 |
|
||
|
||
#### Box 命名空间映射
|
||
|
||
| AppKVStore Box | KvStorage 新方案 | 说明 |
|
||
|---------------|-----------------|------|
|
||
| `HiveBoxNames.app` | `KvStorage` 默认 box | 通用数据 |
|
||
| `HiveBoxNames.searchHistory` | `KvStorage` box 参数 | 搜索历史 |
|
||
| `HiveBoxNames.toolUsage` | `KvStorage` box 参数 | 工具统计 |
|
||
| `HiveBoxNames.userPrefs` | `KvStorage` box 参数 | 用户偏好 |
|
||
| `HiveBoxNames.feedCache` | `KvStorage` box 参数 | Feed 缓存 |
|
||
| `HiveBoxNames.offlineQueue` | `KvStorage` box 参数 | 离线队列 |
|
||
| `HiveBoxNames.cacheConfig` | `KvStorage` box 参数 | 缓存配置 |
|
||
| `HiveBoxNames.chatMessages` | Database (Drift) | 聊天记录迁移到结构化存储 |
|
||
|
||
### 2.4 迁移步骤
|
||
|
||
#### Phase 1: 增强 KvStorage(1-2天)
|
||
|
||
1. **将 `KvStorage` 底层从 SharedPreferences 切换为 Hive**
|
||
```dart
|
||
// 改造后的 KvStorage
|
||
class KvStorage {
|
||
KvStorage._();
|
||
|
||
static bool _initialized = false;
|
||
|
||
/// 初始化 (main 中调用,替代原 KvStorage.init + AppKVStore.init)
|
||
static Future<void> init() async {
|
||
if (_initialized) return;
|
||
await Hive.initFlutter();
|
||
// 打开所有 Box
|
||
for (final name in HiveBoxNames.all) {
|
||
await Hive.openBox<dynamic>(name);
|
||
}
|
||
// 执行 SP → Hive 数据迁移
|
||
await _migrateFromSharedPreferences();
|
||
_initialized = true;
|
||
}
|
||
|
||
// 通用读写 — 默认 app box
|
||
static String? getString(String key, {String box = HiveBoxNames.app}) =>
|
||
_box(box)?.get(key) as String?;
|
||
|
||
static Future<void> setString(String key, String value, {String box = HiveBoxNames.app}) =>
|
||
_box(box)?.put(key, value) ?? Future.value();
|
||
|
||
// ... 其他方法类似
|
||
}
|
||
```
|
||
|
||
2. **合并 `AppKVStore` 的便捷方法到 `KvStorage`**
|
||
- 搜索历史方法: `getSearchHistory()`, `addSearchHistory()`, `clearSearchHistory()`
|
||
- 工具统计方法: `getToolUsageStats()`, `incrementToolUsage()`, `clearToolUsageStats()`
|
||
- 用户偏好方法: `getUserPref()`, `setUserPref()`, `removeUserPref()`, `clearUserPrefs()`
|
||
|
||
3. **合并 `StorageKeys` 和 `HiveBoxNames`**
|
||
- `StorageKeys` 保留,作为所有键名的统一常量类
|
||
- `HiveBoxNames` 保留,作为所有 Box 名的统一常量类
|
||
|
||
#### Phase 2: 逐文件替换 AppKVStore 调用(2-3天)
|
||
|
||
1. **全局搜索替换** `AppKVStore.getString` → `KvStorage.getString`
|
||
2. **逐文件验证**,确保编译通过
|
||
3. **处理返回值差异**: `AppKVStore.setString` 返回 `Future<void>`,`KvStorage.setString` 返回 `Future<bool>`
|
||
- 大部分调用不使用返回值,无需修改
|
||
- 少数检查返回值的需适配
|
||
|
||
#### Phase 3: 数据迁移(1天)
|
||
|
||
1. **启动时自动迁移**: 在 `KvStorage.init()` 中检测旧 key,自动迁移到新 key
|
||
```dart
|
||
static Future<void> _migrateFromSharedPreferences() async {
|
||
final appBox = Hive.box<dynamic>(HiveBoxNames.app);
|
||
final migrated = appBox.get('_kv_storage_migrated') as bool?;
|
||
if (migrated == true) return;
|
||
|
||
try {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
for (final key in prefs.getKeys()) {
|
||
final value = prefs.get(key);
|
||
if (value is String) {
|
||
appBox.put(key, value);
|
||
} else if (value is int) {
|
||
appBox.put(key, value);
|
||
} else if (value is double) {
|
||
appBox.put(key, value);
|
||
} else if (value is bool) {
|
||
appBox.put(key, value);
|
||
}
|
||
}
|
||
await appBox.put('_kv_storage_migrated', true);
|
||
Log.i('SharedPreferences → Hive 数据迁移完成');
|
||
} catch (e) {
|
||
Log.e('SharedPreferences → Hive 数据迁移失败', e);
|
||
}
|
||
}
|
||
```
|
||
|
||
2. **版本标记**: 迁移完成后写入标记,避免重复迁移
|
||
|
||
#### Phase 4: 清理(0.5天)
|
||
|
||
1. **删除 `AppKVStore`** (`lib/core/storage/app_kv_store.dart`)
|
||
2. **删除 `main.dart` 中的 `AppKVStore.init()` 调用**
|
||
3. **更新 `CHANGELOG.md`**
|
||
|
||
### 2.5 受影响文件清单
|
||
|
||
#### 使用 `AppKVStore` 的文件(共 47 个)
|
||
|
||
| # | 文件路径 | 使用方式 |
|
||
|---|---------|---------|
|
||
| 1 | `lib/features/weather/providers/weather_provider.dart` | 读写偏好 |
|
||
| 2 | `lib/features/mine/settings/providers/theme_settings_provider.dart` | 读写设置 |
|
||
| 3 | `lib/core/services/notification/local_notification_service.dart` | 读写配置 |
|
||
| 4 | `lib/features/mine/settings/presentation/more_settings_page.dart` | 读取设置 |
|
||
| 5 | `lib/features/tool_center/inspiration/presentation/pages/tool/hanzi_tool_page.dart` | 工具统计 |
|
||
| 6 | `lib/features/note/presentation/note_list_page.dart` | 读写偏好 |
|
||
| 7 | `lib/features/mine/settings/presentation/font_management_notifier.dart` | 读写设置 |
|
||
| 8 | `lib/features/home/providers/home_provider.dart` | 读写缓存 |
|
||
| 9 | `lib/features/home/providers/home_feed_mixin.dart` | 读写缓存 |
|
||
| 10 | `lib/core/services/clipboard_monitor_service.dart` | 读写配置 |
|
||
| 11 | `lib/features/auth/services/user_security_service.dart` | 读写安全配置 |
|
||
| 12 | `lib/main.dart` | 初始化 |
|
||
| 13 | `lib/features/mine/settings/providers/general_settings_provider.dart` | 读写设置 |
|
||
| 14 | `lib/features/tool_center/statistics/providers/statistics_provider.dart` | 统计数据 |
|
||
| 15 | `lib/features/home/providers/character_tips_provider.dart` | 读写偏好 |
|
||
| 16 | `lib/features/tool_center/statistics/providers/user_stats_provider.dart` | 统计数据 |
|
||
| 17 | `lib/features/tool_center/inspiration/providers/chat_session_provider.dart` | 聊天会话 |
|
||
| 18 | `lib/core/services/notification/notification_center.dart` | 通知配置 |
|
||
| 19 | `lib/core/services/notification/notification_scheduler.dart` | 调度配置 |
|
||
| 20 | `lib/core/services/performance/performance_orchestrator.dart` | 性能配置 |
|
||
| 21 | `lib/features/mine/settings/providers/sub/sound_settings_provider.dart` | 音效设置 |
|
||
| 22 | `lib/core/storage/app_kv_store.dart` | 自身定义 |
|
||
| 23 | `lib/features/mine/user_center/services/account_insights_service.dart` | 账户数据 |
|
||
| 24 | `lib/features/tool_center/inspiration/services/readlater_folder_service.dart` | 稍后读 |
|
||
| 25 | `lib/features/tool_center/inspiration/services/chat_migration_service.dart` | 聊天迁移 |
|
||
| 26 | `lib/features/tool_center/inspiration/providers/tool_center_provider.dart` | 工具中心 |
|
||
| 27 | `lib/features/tool_center/inspiration/services/readlater_tag_service.dart` | 稍后读标签 |
|
||
| 28 | `lib/features/tool_center/inspiration/providers/chat_provider.dart` | 聊天 |
|
||
| 29 | `lib/features/home/providers/favorite_provider.dart` | 收藏 |
|
||
| 30 | `lib/features/search/providers/search_provider.dart` | 搜索历史 |
|
||
| 31 | `lib/shared/widgets/display/appbar_date_display.dart` | 显示配置 |
|
||
| 32 | `lib/core/services/notification/readlater_reminder_service.dart` | 提醒配置 |
|
||
| 33 | `lib/core/services/readlater/readlater_sync_service.dart` | 同步配置 |
|
||
| 34 | `lib/features/poetry/providers/poetry_provider.dart` | 诗词缓存 |
|
||
| 35 | `lib/features/mine/settings/providers/date_display_provider.dart` | 日期配置 |
|
||
| 36 | `lib/core/services/audio/sfx_service.dart` | 音效配置 |
|
||
| 37 | `lib/core/services/audio/tts_service.dart` | TTS 配置 |
|
||
| 38 | `lib/core/services/network/ip_location_service.dart` | IP 缓存 |
|
||
| 39 | `lib/features/home/providers/character_mood_provider.dart` | 角色情绪 |
|
||
| 40 | `lib/features/countdown/providers/countdown_provider.dart` | 倒计时 |
|
||
| 41 | `lib/features/progress/providers/progress_provider.dart` | 进度 |
|
||
| 42 | `lib/features/auth/providers/auth_provider.dart` | 认证 |
|
||
| 43 | `lib/features/pomodoro/providers/pomodoro_provider.dart` | 番茄钟 |
|
||
| 44 | `lib/features/source/providers/source_provider.dart` | 来源 |
|
||
| 45 | `lib/core/services/smart_mode_service.dart` | 智能模式 |
|
||
| 46 | `lib/core/services/data/jinrishici_sdk_service.dart` | 诗词数据 |
|
||
| 47 | `lib/features/search/services/user_preference_service.dart` | 用户偏好 |
|
||
|
||
#### 使用 `KvStorage` 的文件(共 24 个)
|
||
|
||
| # | 文件路径 | 使用方式 |
|
||
|---|---------|---------|
|
||
| 1 | `lib/core/services/device/app_lock_service.dart` | 应用锁配置 |
|
||
| 2 | `lib/features/mine/profile/presentation/profile_page.dart` | 偏好读取 |
|
||
| 3 | `lib/features/mine/settings/presentation/more_settings_page.dart` | 设置读写 |
|
||
| 4 | `lib/core/router/app_router.dart` | 引导页状态 |
|
||
| 5 | `lib/core/services/data/backup_service.dart` | 备份配置 |
|
||
| 6 | `lib/core/layout/ohos_app_shell.dart` | 布局配置 |
|
||
| 7 | `lib/main.dart` | 初始化 |
|
||
| 8 | `lib/features/mine/settings/providers/general_settings_provider.dart` | 通用设置 |
|
||
| 9 | `lib/features/mine/settings/providers/sub/performance_settings_provider.dart` | 性能设置 |
|
||
| 10 | `lib/core/services/catcher2_config_service.dart` | 崩溃配置 |
|
||
| 11 | `lib/features/onboarding/providers/onboarding_provider.dart` | 引导页 |
|
||
| 12 | `lib/features/mine/settings/providers/sub/display_settings_provider.dart` | 显示设置 |
|
||
| 13 | `lib/features/mine/settings/providers/sub/developer_settings_provider.dart` | 开发者设置 |
|
||
| 14 | `lib/features/mine/settings/providers/sub/sound_settings_provider.dart` | 音效设置 |
|
||
| 15 | `lib/features/mine/settings/providers/sub/privacy_settings_provider.dart` | 隐私设置 |
|
||
| 16 | `lib/features/mine/settings/providers/sub/network_settings_provider.dart` | 网络设置 |
|
||
| 17 | `lib/features/mine/settings/providers/sub/general_fields_provider.dart` | 通用字段 |
|
||
| 18 | `lib/core/storage/kv_storage.dart` | 自身定义 |
|
||
| 19 | `lib/features/home/providers/daily_card_style_provider.dart` | 卡片样式 |
|
||
| 20 | `lib/core/services/data/data_export_service.dart` | 数据导出 |
|
||
| 21 | `lib/core/services/network/network_proxy_service.dart` | 代理配置 |
|
||
| 22 | `lib/core/services/device/screen_wake_service.dart` | 屏幕常亮 |
|
||
| 23 | `lib/core/services/device/battery_optimization_service.dart` | 电池优化 |
|
||
| 24 | `lib/core/services/sound_service.dart` | 音效配置 |
|
||
|
||
### 2.6 风险评估
|
||
|
||
| 风险 | 等级 | 缓解措施 |
|
||
|------|------|---------|
|
||
| 数据迁移丢失 | 🔴 高 | 迁移前备份 SP 数据;迁移后不删除 SP 数据,仅标记已迁移 |
|
||
| 返回值类型变化 | 🟡 中 | `AppKVStore.setString` 返回 `Future<void>`,`KvStorage` 返回 `Future<bool>`,需逐文件检查 |
|
||
| Hive Box 未打开 | 🟡 中 | `KvStorage.init()` 中确保所有 Box 都已打开;`_box()` 方法增加 null 保护 |
|
||
| macOS SecureStorage 降级 | 🟡 中 | `SecureStorage` 在 macOS 上降级为 SP,需确保与 `KvStorage` 不冲突(key 前缀隔离) |
|
||
| 并发读写 | 🟢 低 | Hive 天然支持同步读写,无并发问题 |
|
||
| 编译错误 | 🟢 低 | 方法签名基本一致,全局替换后逐文件验证即可 |
|
||
|
||
---
|
||
|
||
## 三、问题2: 服务层全是静态方法
|
||
|
||
### 3.1 现状分析
|
||
|
||
核心服务全部使用静态方法或单例模式,无法注入依赖、无法单元测试、无法模拟:
|
||
|
||
| 服务 | 模式 | 文件路径 | 问题 |
|
||
|------|------|---------|------|
|
||
| `AppLockService` | 静态方法 + ValueNotifier | `lib/core/services/device/app_lock_service.dart` | 无法注入 `LocalAuthentication`,无法 mock 生物识别 |
|
||
| `HomeWidgetService` | 单例 + 实例方法 | `lib/core/services/data/home_widget_service.dart` | `WidgetNotifier` 直接调用 `HomeWidgetService.instance`,紧耦合 |
|
||
| `SoundService` | 静态方法 | `lib/core/services/sound_service.dart` | 无法注入 `AudioPlayer`,无法 mock 音效播放 |
|
||
| `CrashLogService` | 单例 + 实例方法 | `lib/core/services/crash_log_service.dart` | 无法注入文件系统,无法 mock 日志存储 |
|
||
| `WallpaperService` | 工厂单例 | (散布于各处) | 多种实例化方式混用 |
|
||
|
||
**典型问题代码**:
|
||
|
||
```dart
|
||
// AppLockService — 全静态,无法替换依赖
|
||
class AppLockService {
|
||
AppLockService._();
|
||
|
||
static final LocalAuthentication _localAuth = LocalAuthentication(); // 硬编码依赖
|
||
static final _lockNotifier = ValueNotifier<bool>(false); // 自己管理状态
|
||
|
||
static bool get isEnabled => KvStorage.getBool(_keyEnabled) ?? false; // 直接读存储
|
||
static void setEnabled(bool v) {
|
||
KvStorage.setBool(_keyEnabled, v); // 直接写存储
|
||
}
|
||
|
||
static Future<bool> authenticateBiometric({...}) async {
|
||
// 无法在测试中 mock _localAuth
|
||
final didAuthenticate = await _localAuth.authenticate(...);
|
||
}
|
||
}
|
||
```
|
||
|
||
```dart
|
||
// WidgetNotifier — 直接引用单例,紧耦合
|
||
class WidgetNotifier extends Notifier<WidgetState> {
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = HomeWidgetService.instance; // 紧耦合!
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
}
|
||
}
|
||
```
|
||
|
||
**问题影响**:
|
||
- **无法单元测试**: 静态方法无法 mock,测试时必须依赖真实实现
|
||
- **无法替换依赖**: `LocalAuthentication`、`AudioPlayer` 等硬编码在服务内部
|
||
- **状态管理混乱**: `AppLockService` 自己用 `ValueNotifier` 管状态,绕过了 Riverpod
|
||
- **初始化顺序依赖**: 静态方法依赖 `KvStorage.init()` 先执行,否则崩溃
|
||
|
||
### 3.2 目标架构
|
||
|
||
将服务改为 **Riverpod Provider** 模式,实现依赖注入和可测试性:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ 服务层架构 (Riverpod Provider) │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ UI Layer (Widget) │ │
|
||
│ │ ref.watch(appLockProvider) │ │
|
||
│ │ ref.read(appLockProvider.notifier).authenticate() │ │
|
||
│ └──────────────────────┬──────────────────────────────┘ │
|
||
│ │ ref.watch / ref.read │
|
||
│ ┌──────────────────────▼──────────────────────────────┐ │
|
||
│ │ State Layer (Riverpod Notifier) │ │
|
||
│ │ AppLockNotifier extends _$AppLock │ │
|
||
│ │ - 持有状态 (AppLockState) │ │
|
||
│ │ - 调用 Service 方法 │ │
|
||
│ │ - 更新 state │ │
|
||
│ └──────────────────────┬──────────────────────────────┘ │
|
||
│ │ 调用 │
|
||
│ ┌──────────────────────▼──────────────────────────────┐ │
|
||
│ │ Service Layer (纯业务逻辑,可注入) │ │
|
||
│ │ AppLockService(ref) │ │
|
||
│ │ - 依赖通过 Provider 注入 │ │
|
||
│ │ - 不持有 UI 状态 │ │
|
||
│ │ - 返回结果给 Notifier │ │
|
||
│ └──────────────────────┬──────────────────────────────┘ │
|
||
│ │ 读写 │
|
||
│ ┌──────────────────────▼──────────────────────────────┐ │
|
||
│ │ Data Layer (KvStorage / SecureStorage / Database) │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.3 迁移步骤
|
||
|
||
#### 优先迁移顺序
|
||
|
||
1. **AppLockService** → 最复杂,有 ValueNotifier + 生物识别 + 多种解锁方式
|
||
2. **WallpaperService** → 中等复杂度
|
||
3. **HomeWidgetService** → 已有 WidgetNotifier,需解耦
|
||
4. **SoundService** → 简单,静态方法改 Provider
|
||
5. **CrashLogService** → 简单,单例改 Provider
|
||
|
||
#### 通用迁移模式
|
||
|
||
每个服务的迁移遵循以下步骤:
|
||
|
||
1. **创建 State 数据类**
|
||
2. **创建 Riverpod Notifier**
|
||
3. **改造 Service 为可注入类**
|
||
4. **UI 层从 Provider 获取状态**
|
||
5. **旧静态方法标记 @deprecated**
|
||
6. **验证通过后删除旧方法**
|
||
|
||
### 3.4 代码示例
|
||
|
||
#### 示例1: AppLockService 迁移
|
||
|
||
**Before** (全静态):
|
||
|
||
```dart
|
||
class AppLockService {
|
||
AppLockService._();
|
||
static final LocalAuthentication _localAuth = LocalAuthentication();
|
||
static final _lockNotifier = ValueNotifier<bool>(false);
|
||
static bool _isLocked = false;
|
||
|
||
static ValueNotifier<bool> get lockNotifier => _lockNotifier;
|
||
static bool get isLocked => _isLocked;
|
||
|
||
static void lock() {
|
||
_isLocked = true;
|
||
_lockNotifier.value = true;
|
||
}
|
||
|
||
static Future<bool> authenticateBiometric({...}) async {
|
||
final didAuthenticate = await _localAuth.authenticate(...);
|
||
if (didAuthenticate) { unlock(); }
|
||
return didAuthenticate;
|
||
}
|
||
}
|
||
|
||
// UI 使用
|
||
ValueListenableBuilder<bool>(
|
||
valueListenable: AppLockService.lockNotifier,
|
||
builder: (_, isLocked, __) => isLocked ? LockScreen() : Content(),
|
||
)
|
||
```
|
||
|
||
**After** (Riverpod Provider):
|
||
|
||
```dart
|
||
// 1. State 数据类
|
||
@freezed
|
||
class AppLockState with _$AppLockState {
|
||
const factory AppLockState({
|
||
@Default(false) bool isEnabled,
|
||
@Default(AppLockMethod.none) AppLockMethod method,
|
||
@Default(false) bool isLocked,
|
||
@Default(false) bool isAuthenticating,
|
||
@Default(0) int failedAttempts,
|
||
DateTime? lockoutUntil,
|
||
}) = _AppLockState;
|
||
}
|
||
|
||
// 2. Service 改为可注入类
|
||
class AppLockService {
|
||
final LocalAuthentication _localAuth;
|
||
final KvStorage _storage;
|
||
final SecureStorage _secureStorage;
|
||
|
||
AppLockService({
|
||
required LocalAuthentication localAuth,
|
||
required KvStorage storage,
|
||
required SecureStorage secureStorage,
|
||
}) : _localAuth = localAuth,
|
||
_storage = storage,
|
||
_secureStorage = secureStorage;
|
||
|
||
Future<bool> authenticateBiometric({String reason = '请验证身份以解锁闲言'}) async {
|
||
final isSupported = await _localAuth.isDeviceSupported();
|
||
if (!isSupported) return false;
|
||
return await _localAuth.authenticate(localizedReason: reason);
|
||
}
|
||
|
||
Future<String?> getStoredPattern() => _secureStorage.read(_keyPattern);
|
||
Future<String?> getStoredPin() => _secureStorage.read(_keyPin);
|
||
|
||
Future<bool> verifyPattern(String pattern) async {
|
||
final stored = await _secureStorage.read(_keyPattern);
|
||
return pattern == stored;
|
||
}
|
||
|
||
Future<bool> verifyPin(String pin) async {
|
||
final stored = await _secureStorage.read(_keyPin);
|
||
return pin == stored;
|
||
}
|
||
}
|
||
|
||
// 3. Provider
|
||
final appLockServiceProvider = Provider<AppLockService>((ref) {
|
||
return AppLockService(
|
||
localAuth: LocalAuthentication(),
|
||
storage: KvStorage.instance,
|
||
secureStorage: SecureStorage.instance,
|
||
);
|
||
});
|
||
|
||
@riverpod
|
||
class AppLock extends _$AppLock {
|
||
@override
|
||
AppLockState build() {
|
||
final service = ref.read(appLockServiceProvider);
|
||
return AppLockState(
|
||
isEnabled: service._storage.getBool(_keyEnabled) ?? false,
|
||
method: AppLockMethod.fromId(service._storage.getString(_keyMethod) ?? 'none'),
|
||
);
|
||
}
|
||
|
||
Future<void> authenticate() async {
|
||
final service = ref.read(appLockServiceProvider);
|
||
state = state.copyWith(isAuthenticating: true);
|
||
|
||
try {
|
||
bool success = false;
|
||
switch (state.method) {
|
||
case AppLockMethod.biometric:
|
||
success = await service.authenticateBiometric();
|
||
break;
|
||
case AppLockMethod.pattern:
|
||
// UI 层处理手势输入,调用 verifyPattern
|
||
break;
|
||
case AppLockMethod.pin:
|
||
// UI 层处理密码输入,调用 verifyPin
|
||
break;
|
||
case AppLockMethod.none:
|
||
success = true;
|
||
}
|
||
|
||
if (success) {
|
||
state = state.copyWith(
|
||
isLocked: false,
|
||
failedAttempts: 0,
|
||
lockoutUntil: null,
|
||
);
|
||
} else {
|
||
final attempts = state.failedAttempts + 1;
|
||
state = state.copyWith(
|
||
failedAttempts: attempts,
|
||
lockoutUntil: attempts >= 5
|
||
? DateTime.now().add(const Duration(minutes: 5))
|
||
: null,
|
||
);
|
||
}
|
||
} finally {
|
||
state = state.copyWith(isAuthenticating: false);
|
||
}
|
||
}
|
||
|
||
void lock() {
|
||
if (!state.isEnabled) return;
|
||
state = state.copyWith(isLocked: true);
|
||
}
|
||
|
||
void unlock() {
|
||
state = state.copyWith(isLocked: false, failedAttempts: 0, lockoutUntil: null);
|
||
}
|
||
|
||
void setEnabled(bool v) {
|
||
ref.read(appLockServiceProvider)._storage.setBool(_keyEnabled, v);
|
||
state = state.copyWith(isEnabled: v);
|
||
if (!v) unlock();
|
||
}
|
||
}
|
||
|
||
// UI 使用
|
||
class AppLockGate extends ConsumerWidget {
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final lockState = ref.watch(appLockProvider);
|
||
if (lockState.isLocked) return const LockScreen();
|
||
return const Content();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 示例2: SoundService 迁移
|
||
|
||
**Before**:
|
||
|
||
```dart
|
||
class SoundService {
|
||
SoundService._();
|
||
static final AudioPlayer _player = AudioPlayer();
|
||
static bool _enabled = true;
|
||
|
||
static Future<void> playClick() => _play('click');
|
||
}
|
||
```
|
||
|
||
**After**:
|
||
|
||
```dart
|
||
@riverpod
|
||
class Sound extends _$Sound {
|
||
@override
|
||
SoundState build() {
|
||
return SoundState(
|
||
enabled: KvStorage.getBool('general_sound') ?? true,
|
||
effectType: SoundEffectType.fromId(
|
||
KvStorage.getString('general_sound_effect') ?? 'standard',
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> playClick() => _play('click');
|
||
Future<void> playToggle() => _play('toggle');
|
||
Future<void> playSuccess() => _play('success');
|
||
|
||
void setEnabled(bool v) {
|
||
KvStorage.setBool('general_sound', v);
|
||
state = state.copyWith(enabled: v);
|
||
}
|
||
|
||
Future<void> _play(String action) async {
|
||
if (!state.enabled) return;
|
||
try {
|
||
final player = ref.read(audioPlayerProvider);
|
||
await player.stop();
|
||
await player.play(AssetSource('assets/sounds/${state.effectType.id}_$action.mp3'));
|
||
} catch (e) {
|
||
Log.w('音效播放失败($action): $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
// AudioPlayer 作为可注入依赖
|
||
final audioPlayerProvider = Provider<AudioPlayer>((ref) {
|
||
final player = AudioPlayer();
|
||
ref.onDispose(() => player.dispose());
|
||
return player;
|
||
});
|
||
```
|
||
|
||
#### 示例3: HomeWidgetService 迁移 — 解耦 WidgetNotifier
|
||
|
||
**Before** (紧耦合):
|
||
|
||
```dart
|
||
class WidgetNotifier extends Notifier<WidgetState> {
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = HomeWidgetService.instance; // 紧耦合!
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
}
|
||
}
|
||
```
|
||
|
||
**After** (依赖注入):
|
||
|
||
```dart
|
||
// HomeWidgetService 改为 Provider
|
||
final homeWidgetServiceProvider = Provider<HomeWidgetService>((ref) {
|
||
return HomeWidgetService();
|
||
});
|
||
|
||
// WidgetNotifier 通过 ref 获取 Service
|
||
class WidgetNotifier extends Notifier<WidgetState> {
|
||
@override
|
||
WidgetState build() => const WidgetState();
|
||
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = ref.read(homeWidgetServiceProvider); // 依赖注入!
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.5 受影响文件清单
|
||
|
||
| 服务 | 当前模式 | 目标模式 | 涉及文件 |
|
||
|------|---------|---------|---------|
|
||
| `AppLockService` | 静态方法 + ValueNotifier | Riverpod Notifier | `app_lock_service.dart`, `app_lock_overlay.dart`, `app_lock_settings_page.dart`, `app.dart`, `ohos_app_shell.dart` |
|
||
| `HomeWidgetService` | 单例 | Riverpod Provider | `home_widget_service.dart`, `widget_provider.dart`, `app.dart`, `main.dart`, `home_feed_mixin.dart` |
|
||
| `SoundService` | 静态方法 | Riverpod Notifier | `sound_service.dart`, `sound_settings_provider.dart` |
|
||
| `CrashLogService` | 单例 | Riverpod Provider | `crash_log_service.dart`, `crash_log_page.dart`, `catcher2_config_service.dart` |
|
||
| `WidgetNotifier` | Notifier + 直接引用单例 | Notifier + Provider 注入 | `widget_provider.dart` |
|
||
|
||
### 3.6 风险评估
|
||
|
||
| 风险 | 等级 | 缓解措施 |
|
||
|------|------|---------|
|
||
| 迁移期间功能回归 | 🔴 高 | 保留旧静态方法标记 `@deprecated`,新旧并行运行,验证后删除 |
|
||
| Riverpod 初始化顺序 | 🟡 中 | 确保 `ProviderScope` 在 `main()` 中最早创建 |
|
||
| 生物识别平台差异 | 🟡 中 | `LocalAuthentication` 在某些平台不支持,Provider 中需处理异常 |
|
||
| freezed 代码生成 | 🟢 低 | 项目已有 freezed 依赖,运行 `dart run build_runner build` 即可 |
|
||
| 性能影响 | 🟢 低 | Riverpod Provider 是懒加载的,不会增加启动时间 |
|
||
|
||
---
|
||
|
||
## 四、问题3: Provider 与 Service 职责不清
|
||
|
||
### 4.1 现状分析
|
||
|
||
项目中存在 **3 种不同的状态通知模式**混用:
|
||
|
||
| 模式 | 使用者 | 问题 |
|
||
|------|-------|------|
|
||
| `ValueNotifier` | `AppLockService._lockNotifier` | 服务自己管状态,绕过 Riverpod |
|
||
| `ChangeNotifier` | `EditorThemeNotifier`, `CanvasEngine`, `LayerManagerService` | 编辑器模块独有,与 Riverpod 不互通 |
|
||
| `Riverpod Notifier` | `WidgetNotifier`, 各种 Provider | 正确模式,但部分 Provider 直接引用单例 |
|
||
|
||
**典型问题代码**:
|
||
|
||
```dart
|
||
// 问题1: WidgetNotifier 直接调用 HomeWidgetService.instance
|
||
class WidgetNotifier extends Notifer<WidgetState> {
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = HomeWidgetService.instance; // 紧耦合
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
}
|
||
}
|
||
|
||
// 问题2: AppLockService 用 ValueNotifier 通知 UI
|
||
class AppLockService {
|
||
static final _lockNotifier = ValueNotifier<bool>(false);
|
||
static ValueNotifier<bool> get lockNotifier => _lockNotifier;
|
||
// UI 需要 ValueListenableBuilder 监听,与 Riverpod 体系不一致
|
||
}
|
||
|
||
// 问题3: 编辑器用 ChangeNotifier
|
||
class EditorThemeNotifier extends ChangeNotifier {
|
||
void updateTheme(ThemeData theme) {
|
||
_theme = theme;
|
||
notifyListeners(); // 手动通知,容易遗漏
|
||
}
|
||
}
|
||
```
|
||
|
||
**问题影响**:
|
||
- **UI 层需要适配多种监听方式**: `ref.watch` / `ValueListenableBuilder` / `ListenableBuilder`
|
||
- **状态来源不统一**: 同一个功能的状态可能来自 Service/Notifier/Provider
|
||
- **调试困难**: 不知道状态变更的来源和传播路径
|
||
- **内存泄漏**: `ValueNotifier` 和 `ChangeNotifier` 需要手动 dispose,容易遗漏
|
||
|
||
### 4.2 目标架构
|
||
|
||
统一为 **4 层架构**,每层职责清晰:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 4 层架构 │
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Layer 1: UI Layer (Widget) │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ 职责: 渲染界面、响应交互 │ │
|
||
│ │ 规则: │ │
|
||
│ │ - 只通过 ref.watch / ref.read 与 State Layer 交互 │ │
|
||
│ │ - 禁止直接调用 Service │ │
|
||
│ │ - 禁止直接读写 Storage │ │
|
||
│ │ - 禁止使用 ValueListenableBuilder 监听 Service │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │ ref.watch / ref.read │
|
||
│ Layer 2: State Layer (Riverpod Notifier) │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ 职责: 持有状态、调用 Service、通知 UI │ │
|
||
│ │ 规则: │ │
|
||
│ │ - 所有 UI 状态必须通过 Notifier 持有 │ │
|
||
│ │ - 调用 Service 获取/修改数据 │ │
|
||
│ │ - Service 返回结果后更新 state │ │
|
||
│ │ - 禁止在 Notifier 中直接操作 Storage │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │ 调用 │
|
||
│ Layer 3: Service Layer (纯业务逻辑) │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ 职责: 执行业务逻辑、读写数据 │ │
|
||
│ │ 规则: │ │
|
||
│ │ - 不持有 UI 状态 (无 ValueNotifier/ChangeNotifier) │ │
|
||
│ │ - 不直接通知 UI │ │
|
||
│ │ - 返回结果给 Notifier,由 Notifier 决定如何更新状态 │ │
|
||
│ │ - 通过 Provider 注入依赖 │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │ 读写 │
|
||
│ Layer 4: Data Layer (KvStorage / SecureStorage / Database) │
|
||
│ ┌────────────────────────────────────────────────────────┐ │
|
||
│ │ 职责: 数据持久化 │ │
|
||
│ │ 规则: │ │
|
||
│ │ - 只有 Service 可以调用 │ │
|
||
│ │ - Notifier 和 UI 禁止直接调用 │ │
|
||
│ └────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**数据流方向**:
|
||
|
||
```
|
||
UI → ref.read(notifier.action) → Notifier.action()
|
||
→ Service.doSomething() → Storage.read/write
|
||
← Service 返回结果
|
||
← Notifier 更新 state
|
||
← UI 通过 ref.watch 自动重建
|
||
```
|
||
|
||
### 4.3 迁移步骤
|
||
|
||
#### Step 1: 消除 ValueNotifier(1-2天)
|
||
|
||
将所有 `ValueNotifier` 改为 Riverpod `StateNotifier` 或 `Notifier`:
|
||
|
||
| 当前 | 目标 | 涉及文件 |
|
||
|------|------|---------|
|
||
| `AppLockService._lockNotifier` | `appLockProvider` 的 `state.isLocked` | `app_lock_service.dart`, `app_lock_overlay.dart`, `app.dart` |
|
||
| `ShaderCardBackground._notifier` | 保留(纯渲染层,非业务状态) | `shader_card_background.dart` |
|
||
| `AppBarCharacterSprite._notifier` | 保留(纯渲染层,非业务状态) | `appbar_character_sprite.dart` |
|
||
|
||
**判断标准**: 纯渲染层的 `ValueNotifier`(如动画、Canvas 重绘)可以保留,因为它们不属于业务状态。业务状态(如锁定/解锁、设置变更)必须迁移到 Riverpod。
|
||
|
||
#### Step 2: 消除 ChangeNotifier(2-3天)
|
||
|
||
将所有 `ChangeNotifier` 改为 Riverpod `AsyncNotifier`:
|
||
|
||
| 当前 | 目标 | 涉及文件 |
|
||
|------|------|---------|
|
||
| `EditorThemeNotifier` | `editorThemeProvider` | `editor_theme_notifier.dart`, `editor_theme_service.dart` |
|
||
| `CanvasEngine` | `canvasProvider` | `canvas_engine.dart` |
|
||
| `LayerManagerService` | `layerManagerProvider` | `layer_manager_service.dart` |
|
||
| `Scene3DService` | `scene3DProvider` | `scene_3d_service.dart` |
|
||
| `SheetAnimationNotifier` | 保留(纯 UI 动画) | `sheet_animation_notifier.dart` |
|
||
|
||
**注意**: 编辑器模块的 `ChangeNotifier` 较复杂,建议在编辑器模块整体重构时一并处理,不要单独迁移。
|
||
|
||
#### Step 3: 解耦 Provider 与 Service(1-2天)
|
||
|
||
将 Provider 中直接引用 Service 单例的代码改为通过 `ref.read()` 注入:
|
||
|
||
| 当前 | 目标 | 涉及文件 |
|
||
|------|------|---------|
|
||
| `WidgetNotifier` → `HomeWidgetService.instance` | `ref.read(homeWidgetServiceProvider)` | `widget_provider.dart` |
|
||
| `CharacterMoodNotifier` → `AppKVStore.xxx` | `ref.read(kvStorageProvider)` | `character_mood_provider.dart` |
|
||
| `SearchProvider` → `AppKVStore.getSearchHistory()` | `ref.read(searchHistoryServiceProvider)` | `search_provider.dart` |
|
||
|
||
### 4.4 代码示例
|
||
|
||
#### 示例1: ValueNotifier → Riverpod Notifier
|
||
|
||
**Before**:
|
||
|
||
```dart
|
||
// Service 自己管状态
|
||
class AppLockService {
|
||
static final _lockNotifier = ValueNotifier<bool>(false);
|
||
static ValueNotifier<bool> get lockNotifier => _lockNotifier;
|
||
|
||
static void lock() {
|
||
_isLocked = true;
|
||
_lockNotifier.value = true; // 直接通知 UI
|
||
}
|
||
}
|
||
|
||
// UI 用 ValueListenableBuilder 监听
|
||
ValueListenableBuilder<bool>(
|
||
valueListenable: AppLockService.lockNotifier,
|
||
builder: (_, isLocked, __) {
|
||
if (isLocked) return const LockScreen();
|
||
return const Content();
|
||
},
|
||
)
|
||
```
|
||
|
||
**After**:
|
||
|
||
```dart
|
||
// Notifier 管状态,Service 只做业务逻辑
|
||
@riverpod
|
||
class AppLock extends _$AppLock {
|
||
@override
|
||
AppLockState build() => AppLockState.initial();
|
||
|
||
void lock() {
|
||
if (!state.isEnabled) return;
|
||
state = state.copyWith(isLocked: true);
|
||
// Service 不需要知道 UI 状态
|
||
}
|
||
}
|
||
|
||
// UI 用 ref.watch 监听
|
||
class AppLockGate extends ConsumerWidget {
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final isLocked = ref.watch(appLockProvider.select((s) => s.isLocked));
|
||
if (isLocked) return const LockScreen();
|
||
return const Content();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 示例2: ChangeNotifier → Riverpod AsyncNotifier
|
||
|
||
**Before**:
|
||
|
||
```dart
|
||
class EditorThemeNotifier extends ChangeNotifier {
|
||
ThemeData _theme = ThemeData.light();
|
||
ThemeData get theme => _theme;
|
||
|
||
void updateTheme(ThemeData theme) {
|
||
_theme = theme;
|
||
notifyListeners(); // 手动通知
|
||
}
|
||
}
|
||
|
||
// UI
|
||
ListenableBuilder(
|
||
listenable: editorThemeNotifier,
|
||
builder: (_, __) => ThemedContent(theme: editorThemeNotifier.theme),
|
||
)
|
||
```
|
||
|
||
**After**:
|
||
|
||
```dart
|
||
@riverpod
|
||
class EditorTheme extends _$EditorTheme {
|
||
@override
|
||
Future<ThemeData> build() async {
|
||
final service = ref.read(editorThemeServiceProvider);
|
||
return service.loadTheme();
|
||
}
|
||
|
||
Future<void> updateTheme(ThemeData theme) async {
|
||
final service = ref.read(editorThemeServiceProvider);
|
||
await service.saveTheme(theme);
|
||
state = AsyncData(theme);
|
||
}
|
||
}
|
||
|
||
// UI
|
||
class ThemedContentWrapper extends ConsumerWidget {
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final themeAsync = ref.watch(editorThemeProvider);
|
||
return themeAsync.when(
|
||
data: (theme) => ThemedContent(theme: theme),
|
||
loading: () => const CircularProgressIndicator(),
|
||
error: (e, _) => Text('Error: $e'),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 示例3: 解耦 Provider 与 Service
|
||
|
||
**Before**:
|
||
|
||
```dart
|
||
class WidgetNotifier extends Notifier<WidgetState> {
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = HomeWidgetService.instance; // 紧耦合
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
}
|
||
}
|
||
```
|
||
|
||
**After**:
|
||
|
||
```dart
|
||
// Service 注册为 Provider
|
||
final homeWidgetServiceProvider = Provider<HomeWidgetService>((ref) {
|
||
return HomeWidgetService();
|
||
});
|
||
|
||
// Notifier 通过 ref 获取 Service
|
||
class WidgetNotifier extends Notifier<WidgetState> {
|
||
@override
|
||
WidgetState build() => const WidgetState();
|
||
|
||
Future<void> addWidget(WidgetType type) async {
|
||
final service = ref.read(homeWidgetServiceProvider); // 依赖注入
|
||
await service.init();
|
||
await service.updateWidget(type);
|
||
state = state.copyWith(installedWidgets: {...state.installedWidgets, type});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.5 受影响文件清单
|
||
|
||
| 模式 | 文件 | 迁移目标 |
|
||
|------|------|---------|
|
||
| ValueNotifier | `lib/core/services/device/app_lock_service.dart` | Riverpod Notifier |
|
||
| ValueNotifier | `lib/shared/widgets/animation/shader_card_background.dart` | 保留(纯渲染) |
|
||
| ValueNotifier | `lib/shared/widgets/animation/appbar_character_sprite.dart` | 保留(纯渲染) |
|
||
| ChangeNotifier | `lib/editor/services/core/editor_theme_notifier.dart` | Riverpod AsyncNotifier |
|
||
| ChangeNotifier | `lib/features/file_transfer/collaboration/canvas/services/canvas_engine.dart` | Riverpod Provider |
|
||
| ChangeNotifier | `lib/editor/services/core/layer_manager_service.dart` | Riverpod Provider |
|
||
| ChangeNotifier | `lib/editor/services/3d/scene_3d_service.dart` | Riverpod Provider |
|
||
| ChangeNotifier | `lib/core/utils/ui/sheet_animation_notifier.dart` | 保留(纯 UI 动画) |
|
||
| 紧耦合 | `lib/features/widget/providers/widget_provider.dart` | 解耦 Service |
|
||
| 直接读存储 | `lib/features/home/providers/character_mood_provider.dart` | 通过 Service |
|
||
| 直接读存储 | `lib/features/search/providers/search_provider.dart` | 通过 Service |
|
||
|
||
### 4.6 风险评估
|
||
|
||
| 风险 | 等级 | 缓解措施 |
|
||
|------|------|---------|
|
||
| 编辑器模块 ChangeNotifier 复杂 | 🔴 高 | 编辑器模块单独重构,不在此轮迁移 |
|
||
| ValueNotifier → Notifier 行为差异 | 🟡 中 | `ValueNotifier` 是同步通知,`Notifier` 也是同步更新 state,行为一致 |
|
||
| ChangeNotifier → AsyncNotifier 异步化 | 🟡 中 | 部分 ChangeNotifier 是同步的,改为 AsyncNotifier 后 UI 需要处理 loading 状态 |
|
||
| 纯渲染 ValueNotifier 误迁移 | 🟡 中 | 严格区分业务状态和渲染状态,纯渲染层保留 ValueNotifier |
|
||
| ref.read 时序问题 | 🟢 低 | 确保 `ref.read` 在回调中使用,不在 `build` 中使用 |
|
||
|
||
---
|
||
|
||
## 五、实施优先级与时间线
|
||
|
||
```
|
||
Week 1-2: 问题1 — 存储碎片化
|
||
├── Day 1-2: Phase 1 — 增强 KvStorage
|
||
├── Day 3-5: Phase 2 — 逐文件替换 AppKVStore
|
||
├── Day 6: Phase 3 — 数据迁移
|
||
└── Day 7: Phase 4 — 清理 + 验证
|
||
|
||
Week 3-4: 问题2 — 服务静态化
|
||
├── Day 1-3: AppLockService → Riverpod Provider
|
||
├── Day 4-5: HomeWidgetService → 解耦
|
||
├── Day 6: SoundService → Riverpod Notifier
|
||
└── Day 7: CrashLogService → Riverpod Provider
|
||
|
||
Week 5-6: 问题3 — 职责不清
|
||
├── Day 1-2: 消除 ValueNotifier (AppLockService)
|
||
├── Day 3-5: 消除 ChangeNotifier (编辑器除外)
|
||
└── Day 6-7: 解耦 Provider 与 Service + 全面验证
|
||
```
|
||
|
||
**总预估**: 6 周(含测试和验证)
|
||
|
||
---
|
||
|
||
## 六、通用迁移检查清单
|
||
|
||
每个模块迁移完成后,需逐项检查:
|
||
|
||
### 编译检查
|
||
- [ ] `dart analyze` 无错误
|
||
- [ ] `flutter build` 成功
|
||
- [ ] 无 `@deprecated` 警告(已清理旧代码)
|
||
|
||
### 功能检查
|
||
- [ ] 旧功能行为不变
|
||
- [ ] 数据迁移正确(旧数据可读、新数据可写)
|
||
- [ ] 前后台切换无崩溃
|
||
- [ ] 鸿蒙端兼容(`TargetPlatform.ohos` 处理)
|
||
|
||
### 架构检查
|
||
- [ ] UI 层不直接调用 Service
|
||
- [ ] UI 层不直接读写 Storage
|
||
- [ ] Service 不持有 UI 状态(无 ValueNotifier/ChangeNotifier)
|
||
- [ ] 所有依赖通过 Provider 注入
|
||
- [ ] 无静态方法直接引用(已迁移到 Provider)
|
||
|
||
### 测试检查
|
||
- [ ] Service 可 mock(依赖注入)
|
||
- [ ] Notifier 可单元测试
|
||
- [ ] 数据迁移有回退机制
|
||
|
||
### 文档检查
|
||
- [ ] `CHANGELOG.md` 已更新
|
||
- [ ] 文件头部注释已更新
|
||
- [ ] 本文档已标记完成状态
|
||
|
||
---
|
||
|
||
> **文档维护说明**: 每完成一个迁移步骤,请在此文档对应位置标记 ✅。全部完成后,将此文档归档到 `docs/completed/` 目录。
|