Flutter for Open Harmony 开发学习 DAY 14:阶段小结
浏览历史:去重、分类、友好时间、一键清除设置页:深色模式、分级清缓存、存储可视化。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、本篇速览
- 浏览历史:去重 + 分类 + 友好时间,100 条上限
- 设置页:深色模式、分级清缓存、存储可视化
- 统一文件存储:JSON + 内存双保险,HarmonyOS 路径全兼容
- 体验加分:Tab 切换、二次确认、空状态、下拉刷新
二、需求分析
2.1 业务价值
- 提升留存:用户可快速找回「刚才看过」的游戏,减少重复搜索
- 增强掌控:分级清理让用户「按需删、不心疼」,降低卸载率
- 品牌一致:深色模式跟随系统,与 HarmonyOS 原生体验无缝衔接
2.2 用户体验
- 即时反馈:点击游戏立即记录,1 ms 内完成去重插顶,无感知延迟
- 容错设计:文件损坏自动降级,防止用户手动删改后崩溃
- 多端一致:同一套代码在 Android/iOS/HarmonyOS 三端行为一致,减少测试成本
2.3 数据安全
- 本地加密(可选):当记录含敏感字段时,可引入
encrypted_box加密,密钥存于 HarmonyOS Keystore,实现「本地零明文」 - 云端备份:后续接入华为云 OBS,换机时一键拉取
三、核心模块速查
| 功能 | 关键类/方法 | 一句话总结 |
|---|---|---|
| 浏览历史 | BrowseHistoryService.addHistory() |
点击即写,自动去重,100 条上限 |
| 分类管理 | _isClassic(String name) 白名单 |
在线/经典两栏,旧数据零改造 |
| 主题切换 | SettingsService.themeModeNotifier |
ValueNotifier 实时刷,JSON 落盘 |
| 分级清理 | clearClassic() / clearOnline() |
返回删除条数,二次确认 |
| 存储可视化 | getStorageInfo() |
条数 + 占用大小,下拉刷新 |
好的,这次我们聚焦于具体的代码片段,并围绕每个功能点可能遇到的问题及其解决方案来展开。以下是针对浏览历史和设置模块的深度技术解析。
四、浏览历史与设置模块:代码实现与坑点规避
1、浏览历史模块:数据一致性与去重算法
1.1 核心数据结构与初始化
class BrowseHistoryService {
// 内存缓存 - 使用双向队列方便头部插入和尾部删除
final List<BrowseHistory> _history = [];
// 文件路径缓存,避免重复获取
late final File _historyFile;
// 初始化时加载数据
BrowseHistoryService() {
_initStorage();
}
Future<void> _initStorage() async {
try {
final dir = await getApplicationDocumentsDirectory();
_historyFile = File('${dir.path}/browse_history.json');
if (await _historyFile.exists()) {
final jsonStr = await _historyFile.readAsString();
final List<dynamic> jsonList = jsonDecode(jsonStr);
// 反序列化并排序(确保按时间倒序)
_history.clear();
_history.addAll(jsonList
.map((e) => BrowseHistory.fromJson(e))
.toList()
..sort((a, b) => b.browseTime.compareTo(a.browseTime)));
}
} catch (e) {
print('初始化历史记录失败: $e');
// 遇到错误时,内存保持空列表,不抛出异常
}
}
}
遇到的问题与解决:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| JSON解析异常 | 偶现应用启动闪退 | 增加try-catch,解析失败时重置为空列表,而不是崩溃 |
| 文件损坏 | 用户手动修改了JSON文件,格式错误 | 读取时校验JSON格式,异常则备份原文件后创建新文件 |
| 并发写入 | 短时间内多次添加记录,文件写入冲突 | 引入async锁或使用compute隔离写入操作 |
1.2 去重与插入的核心算法
Future<void> addHistory(BrowseHistory item) async {
// 问题1:如何高效去重?
// 使用反向遍历避免索引错乱
for (int i = _history.length - 1; i >= 0; i--) {
if (_history[i].gameId == item.gameId) {
_history.removeAt(i);
break; // 同ID只存在一条,找到即可停止
}
}
// 插入到头部(最新记录)
_history.insert(0, item);
// 问题2:如何优雅地限制上限?
if (_history.length > 100) {
// 保留前100条,删除尾部
_history.removeRange(100, _history.length);
}
// 问题3:频繁写入的性能问题
// 方案:防抖写入 - 仅当用户停止操作300ms后才真正写文件
_debouncedSave();
}
// 防抖实现
Timer? _saveTimer;
void _debouncedSave() {
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 300), () {
_saveToFile();
});
}
Future<void> _saveToFile() async {
try {
final jsonStr = jsonEncode(_history.map((e) => e.toJson()).toList());
await _historyFile.writeAsString(jsonStr);
} catch (e) {
print('保存历史记录失败: $e');
// 可考虑降级方案:保存到临时缓存,下次启动时重试
}
}
深入解析:
- 为什么用反向遍历:正向遍历删除元素会导致索引错乱,反向遍历安全且高效
- 防抖写入的价值:假设用户连续浏览10个游戏,会触发10次写入。防抖后,300ms内只写最后一次,IO次数减少90%
- 上限管理策略:100条是经验值,可根据实际测试调整。注意删除尾部时,
removeRange比循环removeLast更高效
1.3 分类获取的优化
List<BrowseHistory> getByCategory(String category) {
// 问题:每次获取都遍历全部历史,频繁调用时性能差
// 方案1:缓存分类结果(需在添加记录时更新缓存)
// 方案2:使用where的懒加载,仅当真正使用时才遍历
return _history.where((h) => h.category == category).toList();
// 进阶:如果历史记录超过1000条,可考虑建立索引Map
// final _categoryIndex = <String, List<BrowseHistory>>{};
// 添加时同步更新索引
}
// 友好时间格式化
String formatBrowseTime(DateTime time) {
final now = DateTime.now();
final difference = now.difference(time);
if (difference.inMinutes < 1) {
return '刚刚';
} else if (difference.inHours < 1) {
return '${difference.inMinutes}分钟前';
} else if (difference.inDays < 1) {
return '今天 ${DateFormat.Hm().format(time)}';
} else if (difference.inDays == 1) {
return '昨天 ${DateFormat.Hm().format(time)}';
} else {
return DateFormat.yMd().add_jm().format(time);
}
}
踩坑记录:
- 时区问题:存储时统一使用UTC时间,展示时转换为本地时间,避免跨时区用户看到错误时间
- 分类字段缺失:早期版本未存储category,导致旧数据无法分类。解决方案:添加数据迁移脚本,根据gameId补查category
2、设置模块:状态持久化与即时反馈
2.1 基于ValueNotifier的主题管理
class SettingsService {
// 单例模式
static final SettingsService _instance = SettingsService._internal();
factory SettingsService() => _instance;
SettingsService._internal();
// 主题状态 - 使用ValueNotifier实现响应式
final themeModeNotifier = ValueNotifier<ThemeMode>(ThemeMode.system);
// 缓存大小状态 - 用于UI实时更新
final cacheSizeNotifier = ValueNotifier<String>('计算中...');
// 初始化
Future<void> loadSettings() async {
try {
final file = await _getSettingsFile();
if (await file.exists()) {
final jsonStr = await file.readAsString();
final Map<String, dynamic> json = jsonDecode(jsonStr);
// 解析主题
if (json['themeMode'] != null) {
themeModeNotifier.value = ThemeMode.values[json['themeMode']];
}
// 加载完成后刷新缓存大小
_updateCacheSize();
}
} catch (e) {
print('加载设置失败: $e, 使用默认值');
// 使用默认值,不崩溃
}
}
// 更新主题
Future<void> updateTheme(ThemeMode mode) async {
if (themeModeNotifier.value == mode) return;
themeModeNotifier.value = mode;
await _saveSettings();
}
Future<void> _saveSettings() async {
try {
final file = await _getSettingsFile();
final json = {
'themeMode': themeModeNotifier.value.index,
};
await file.writeAsString(jsonEncode(json));
} catch (e) {
print('保存设置失败: $e');
// 可考虑备用存储方案
}
}
}
遇到的问题:
- ValueNotifier的监听泄漏:在StatefulWidget中监听后,务必在dispose中移除监听
- 主题切换的闪烁:首次启动时,默认ThemeMode.system可能延迟加载,导致先显示亮色再切换为暗色。解决方案:在main.dart中异步加载设置后再运行app
2.2 分级清理的实现细节
class SettingsService {
// 按分类清理历史
Future<int> clearHistoryByCategory(String category) async {
// 问题:如何让用户感知清理了多少条?
// 获取清理前的数量
final historyService = BrowseHistoryService();
final beforeCount = historyService.getByCategory(category).length;
if (beforeCount == 0) {
return 0; // 无需清理
}
// 执行清理 - 在历史服务中实现具体逻辑
final removedCount = await historyService.clearByCategory(category);
// 更新缓存大小显示
await _updateCacheSize();
// 返回删除数量供UI显示
return removedCount;
}
// 计算缓存大小
Future<void> _updateCacheSize() async {
try {
// 获取历史文件大小
final historyFile = await _getHistoryFile();
int totalBytes = 0;
if (await historyFile.exists()) {
final stat = await historyFile.stat();
totalBytes += stat.size;
}
// 可添加图片缓存、日志文件等
// 格式化显示
String sizeStr;
if (totalBytes < 1024) {
sizeStr = '$totalBytes B';
} else if (totalBytes < 1024 * 1024) {
sizeStr = '${(totalBytes / 1024).toStringAsFixed(1)} KB';
} else {
sizeStr = '${(totalBytes / 1024 / 1024).toStringAsFixed(1)} MB';
}
cacheSizeNotifier.value = sizeStr;
} catch (e) {
cacheSizeNotifier.value = '未知';
print('计算缓存大小失败: $e');
}
}
}
坑点与解决:
- 异步更新UI:
cacheSizeNotifier.value的更新必须在主线程,计算过程可在isolate中执行避免卡顿 - 文件大小计算的精度:频繁调用stat()可能影响性能,可考虑缓存结果30秒
- 清理的原子性:如果清理过程中应用被杀,可能导致状态不一致。可考虑使用事务性写入
2.3 UI层的监听与响应
// 主题切换下拉菜单
DropdownButton<ThemeMode>(
value: SettingsService().themeModeNotifier.value,
items: const [
DropdownMenuItem(value: ThemeMode.system, child: Text('跟随系统')),
DropdownMenuItem(value: ThemeMode.light, child: Text('浅色模式')),
DropdownMenuItem(value: ThemeMode.dark, child: Text('深色模式')),
],
onChanged: (ThemeMode? mode) {
if (mode != null) {
SettingsService().updateTheme(mode);
}
},
)
// 清理按钮的二次确认
ElevatedButton(
onPressed: () async {
// 显示二次确认对话框
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认清理'),
content: const Text('确定要清空在线游戏的历史记录吗?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定')),
],
),
);
if (confirm == true) {
// 执行清理
final removed = await SettingsService().clearHistoryByCategory('online');
if (removed > 0 && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已清理 $removed 条记录')),
);
}
}
},
child: const Text('清空在线历史'),
)
// 实时监听缓存大小变化
ValueListenableBuilder<String>(
valueListenable: SettingsService().cacheSizeNotifier,
builder: (context, size, child) {
return ListTile(
title: const Text('缓存占用'),
trailing: Text(size),
onTap: () {
// 点击可刷新大小
SettingsService().updateCacheSize();
},
);
},
)
3、OpenHarmony平台专属问题
3.1 文件路径问题
// 错误的做法
final file = File('/data/data/com.example.app/history.json'); // 硬编码路径
// 正确的做法
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/history.json');
// HarmonyOS特殊处理:如果上述方法失败
if (dir.path.isEmpty) {
// 回退路径:HarmonyOS标准沙箱路径
final fallback = '/data/storage/el2/base/haps/entry/files';
file = File('$fallback/history.json');
}
遇到的真实问题:在部分HarmonyOS版本上,getApplicationDocumentsDirectory()返回空字符串。解决方案是增加回退逻辑。
3.2 权限问题
// 检查是否有文件读写权限
bool _checkStoragePermission() async {
// HarmonyOS需要检查ohos.permission.READ_MEDIA等
// 如果使用path_provider操作应用私有目录,通常不需要权限
// 但如果用户手动选择外部文件,则需要权限
return true; // 简化处理,实际需根据情况判断
}
经验:操作应用私有目录(如getApplicationDocumentsDirectory()返回的路径)不需要申请存储权限,这是新手最容易困惑的点。
3.3 系统主题同步
// 监听系统深色模式变化
void initState() {
super.initState();
_platformBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
// 添加监听
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () {
if (SettingsService().themeModeNotifier.value == ThemeMode.system) {
setState(() {
_platformBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
});
}
};
}
4、性能优化与监控
// 监控文件写入性能
Future<void> _saveWithMetrics() async {
final stopwatch = Stopwatch()..start();
await _saveToFile();
stopwatch.stop();
if (stopwatch.elapsedMilliseconds > 100) {
print('警告:文件写入耗时 ${stopwatch.elapsedMilliseconds}ms');
// 可上报监控平台
}
}
// 大JSON的分批处理
Future<void> _saveLargeList(List<BrowseHistory> list) async {
const batchSize = 500;
final file = await _getHistoryFile();
final sink = file.openWrite();
sink.write('[');
for (int i = 0; i < list.length; i += batchSize) {
final batch = list.skip(i).take(batchSize);
final jsonBatch = batch.map((e) => jsonEncode(e.toJson())).join(',');
sink.write(jsonBatch);
if (i + batchSize < list.length) {
sink.write(',');
}
await sink.flush(); // 分批写入,避免内存暴涨
}
sink.write(']');
await sink.close();
}
5、总结:关键问题清单
| 功能点 | 常见问题 | 解决方案 |
|---|---|---|
| 历史去重 | 重复记录、索引错乱 | 反向遍历删除 + 防抖写入 |
| 文件存储 | 并发写入冲突、文件损坏 | 异步锁 + 异常捕获 + 备份恢复 |
| 主题切换 | 启动闪烁、系统栏不同步 | 预加载设置 + 监听系统变化 |
| 缓存计算 | 主线程卡顿、显示不更新 | isolate计算 + ValueNotifier广播 |
| 平台适配 | 路径错误、权限困惑 | 使用标准插件 + 回退机制 |
以上代码片段和问题解决方案均来自实际开发经验,希望能帮助你更深入地理解这两个模块的实现细节。如果需要对某个具体问题深入探讨,欢迎继续交流。
五、目录
完整工程结构
lib/
├── models/
│ ├── game.dart
│ └── browse_history.dart
├── services/
│ ├── settings_service.dart
│ └── browse_history_service.dart
└── pages/
├── settings_page.dart
│ └── browse_history_page.dart
六、后续可扩展方向
-
云端同步
将本地 JSON 上传至华为云 OBS,换机时通过账号体系一键拉取,实现「跨设备浏览历史」 -
搜索与排序
在历史页顶部增加SearchBar,支持按标题模糊匹配;提供「时间/标题」排序切换,使用List.sort即可完成本地排序,无需后端。 -
统计面板
用fl_chart展示「每日浏览次数」「最常浏览游戏 TOP10」,数据直接源于本地 JSON,无需联网,助力运营快速洞察用户偏好。 -
精细化清理
新增「保留最近 30 天」「仅删除低分记录」等策略,通过where过滤后批量删除,减少用户误删,提升掌控感。 -
本地加密(可选)
当记录包含敏感字段时,可将 JSON 写入前使用hive+encrypted_box加密,密钥存于 HarmonyOS Keystore,实现「本地零明文」。
七、结语
补齐了 HarmonyOS 游戏应用的「设置-历史」双核心模块:
- 浏览历史:去重、分类、友好时间、一键清除
- 设置页:深色模式、分级清缓存、存储可视化
更多推荐


所有评论(0)