【Flutter for OpenHarmony】鸿蒙跨平台训练营 Day 20:备忘录标签系统功能开发
通过这次标签功能的开发,我们不仅掌握了 Flutter 中复杂 UI 状态管理(如 Dialog 中的 State),还深刻体会到了数据模型设计对后续扩展性的影响。从简单的字符串列表重构为对象列表,虽然前期增加了迁移成本,但为后续的颜色自定义、排序等功能打下了坚实基础。组件化思维(Extract Widgets)也让代码结构清晰了许多,以后维护起来会轻松不少。
Day 20:备忘录标签系统功能开发
项目仓库:shhzxt/flutter_OpenHarmony
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
这两天给备忘录项目加了一个标签分类功能,这篇笔记记录了从简单的字符串标签升级到支持自定义颜色的完整对象标签的全过程,以及遇到的坑和学到的知识点。
1. 需求背景
最开始备忘录的标签只是简单的字符串列表(List<String>),用户不能自己选颜色,也不能管理。现在的需求是:
- 用户能自己创建标签,还能选喜欢的背景色。
- 要有个专门的页面管理这些标签(增删改)。
- 主页加个“分类”Tab,能按标签把备忘录归类显示。
2. 核心知识点记录
2.1 数据模型的演进:从 String 到 Object
之前的做法:
备忘录里直接存 tags: ['工作', '生活']。缺点是没法存颜色信息。
改进后的做法:
新建了一个 Tag 模型类。这里学到了如何设计一个既包含数据又包含 UI 属性(颜色)的模型。
class Tag {
final String name;
final int colorValue; // 存 int 而不是 Color 对象,方便 JSON 序列化
// 存取时再转回 Color
Color get color => Color(colorValue);
// JSON 序列化标准写法
factory Tag.fromJson(Map<String, dynamic> json) { ... }
Map<String, dynamic> toJson() { ... }
}
在 Model 层尽量只存基本数据类型(如
int),把Color这种 UI 相关的对象转换放在 getter 里做,这样序列化逻辑会很干净。

2.2 本地存储的“平滑升级” (Data Migration)
这是一个很大的挑战。老用户的本地缓存里存的是 List<String>,新代码跑起来如果直接读 List<Tag> 肯定会崩。
解决方案:数据迁移策略
在 LocalStorageService 里做了一层兼容逻辑:
- 优先尝试读取新版 Key (
user_tags_v2)。 - 如果没读到,去读旧版 Key (
user_tags)。 - 关键点:读到旧版数据后,立刻把它们封装成
Tag对象(给个默认颜色),然后存到新版 Key 里。 - 下次再读就是新版数据了。
// 伪代码逻辑
static Future<List<Tag>> getTags() async {
// 1. 试读新版
if (hasV2Data) return decodeV2();
// 2. 试读旧版并迁移
if (hasV1Data) {
var v1Tags = getV1();
var newTags = v1Tags.map((t) => Tag(name: t, color: defaultColor)).toList();
saveTags(newTags); // 存入新版,完成迁移
return newTags;
}
}
App 开发一定要考虑向后兼容,不能假设用户都是新安装的。这种“读取时迁移”的策略非常实用。
2.3 颜色选择器的实现
在 AlertDialog 里实现颜色选择时,遇到一个状态更新的问题。
问题:在 showDialog 的 content 里直接用 setState 更新选中的颜色,UI 不会变。
原因:Dialog 的 Context 和外面的 Widget 不一样,它需要自己的 State 管理。
解决:使用了 StatefulBuilder 包裹 Dialog 的内容。
showDialog(
context: context,
builder: (context) => StatefulBuilder( // 重点!
builder: (context, setState) { // 这里的 setState 专门用来刷新 Dialog 内部
return AlertDialog(
content: Wrap(
children: colors.map((c) => GestureDetector(
onTap: () => setState(() => _selectedColor = c), // 刷新选中状态
child: ColorCircle(...),
)).toList(),
)
);
}
),
);

2.4 UI 组件复用 (Refactoring)
做“分类页面”时,发现它显示的备忘录卡片和“首页”一模一样。
重构前:直接把首页的代码复制粘贴了一份到分类页。
重构后:把卡片部分抽离成了 MemoCard 组件 (lib/widgets/memo_card.dart)。
Don’t Repeat Yourself (DRY)。一旦发现复制粘贴了超过 10 行代码,就该考虑抽取组件了。
2.5 列表的折叠与展开
分类页面用到了 ExpansionTile 组件,非常适合做这种“点击标题展开内容”的效果。
ExpansionTile(
leading: ColorDot(tag.color), // 左边显示标签颜色点
title: Text(tag.name),
trailing: Text('${memos.length}'), // 右边显示数量
children: memos.map((m) => MemoCard(memo: m)).toList(),
)

3. 总结
通过这次标签功能的开发,我们不仅掌握了 Flutter 中复杂 UI 状态管理(如 Dialog 中的 State),还深刻体会到了数据模型设计对后续扩展性的影响。从简单的字符串列表重构为对象列表,虽然前期增加了迁移成本,但为后续的颜色自定义、排序等功能打下了坚实基础。组件化思维(Extract Widgets)也让代码结构清晰了许多,以后维护起来会轻松不少。
更多推荐



所有评论(0)