本次提交涵盖多项功能优化与重构: 1. 重构pro_image_editor依赖为官方托管版本,移除本地包引用 2. 拆分角色表情枚举至独立文件,优化代码复用性 3. 新增壁纸收藏、预加载、健康检测服务与本地存储支持 4. 完善API响应类型安全检查与排行榜服务能力 5. 新增应用锁设置路由与页面支持 6. 优化路由跳转使用常量路径替代硬编码字符串 7. 新增阅读报告分享功能与设置变更日志服务 8. 修复多处类型转换与空指针风险问题 9. 调整API超时时间优化网络请求表现 10. 统一文件头格式与部分UI组件样式
50 KiB
闲言APP 架构重构规格书
创建时间: 2026-05-24 版本: v1.0 作用: 描述当前架构三大核心问题及其解决方案,供 AI 开发者参考实施 适用范围:
lib/core/和lib/features/全部模块
目录
一、总览
当前项目存在三大架构级问题,彼此关联、相互影响:
┌─────────────────────────────────────────────────────────┐
│ 架构问题关系图 │
│ │
│ 问题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 功能高度重叠:
// 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
理由:
- Hive 性能优于 SharedPreferences(内存级读取 vs 磁盘IO)
- Hive 支持同步读取,SharedPreferences 只有异步
AppKVStore已有 SP→Hive 迁移逻辑,可复用- 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天)
-
将
KvStorage底层从 SharedPreferences 切换为 Hive// 改造后的 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(); // ... 其他方法类似 } -
合并
AppKVStore的便捷方法到KvStorage- 搜索历史方法:
getSearchHistory(),addSearchHistory(),clearSearchHistory() - 工具统计方法:
getToolUsageStats(),incrementToolUsage(),clearToolUsageStats() - 用户偏好方法:
getUserPref(),setUserPref(),removeUserPref(),clearUserPrefs()
- 搜索历史方法:
-
合并
StorageKeys和HiveBoxNamesStorageKeys保留,作为所有键名的统一常量类HiveBoxNames保留,作为所有 Box 名的统一常量类
Phase 2: 逐文件替换 AppKVStore 调用(2-3天)
- 全局搜索替换
AppKVStore.getString→KvStorage.getString - 逐文件验证,确保编译通过
- 处理返回值差异:
AppKVStore.setString返回Future<void>,KvStorage.setString返回Future<bool>- 大部分调用不使用返回值,无需修改
- 少数检查返回值的需适配
Phase 3: 数据迁移(1天)
-
启动时自动迁移: 在
KvStorage.init()中检测旧 key,自动迁移到新 keystatic 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); } } -
版本标记: 迁移完成后写入标记,避免重复迁移
Phase 4: 清理(0.5天)
- 删除
AppKVStore(lib/core/storage/app_kv_store.dart) - 删除
main.dart中的AppKVStore.init()调用 - 更新
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 |
工厂单例 | (散布于各处) | 多种实例化方式混用 |
典型问题代码:
// 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(...);
}
}
// 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 迁移步骤
优先迁移顺序
- AppLockService → 最复杂,有 ValueNotifier + 生物识别 + 多种解锁方式
- WallpaperService → 中等复杂度
- HomeWidgetService → 已有 WidgetNotifier,需解耦
- SoundService → 简单,静态方法改 Provider
- CrashLogService → 简单,单例改 Provider
通用迁移模式
每个服务的迁移遵循以下步骤:
- 创建 State 数据类
- 创建 Riverpod Notifier
- 改造 Service 为可注入类
- UI 层从 Provider 获取状态
- 旧静态方法标记 @deprecated
- 验证通过后删除旧方法
3.4 代码示例
示例1: AppLockService 迁移
Before (全静态):
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):
// 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:
class SoundService {
SoundService._();
static final AudioPlayer _player = AudioPlayer();
static bool _enabled = true;
static Future<void> playClick() => _play('click');
}
After:
@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 (紧耦合):
class WidgetNotifier extends Notifier<WidgetState> {
Future<void> addWidget(WidgetType type) async {
final service = HomeWidgetService.instance; // 紧耦合!
await service.init();
await service.updateWidget(type);
}
}
After (依赖注入):
// 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 直接引用单例 |
典型问题代码:
// 问题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:
// 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:
// 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:
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:
@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:
class WidgetNotifier extends Notifier<WidgetState> {
Future<void> addWidget(WidgetType type) async {
final service = HomeWidgetService.instance; // 紧耦合
await service.init();
await service.updateWidget(type);
}
}
After:
// 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/目录。