在鸿蒙平台上用 Flutter 做开发,状态管理是绕不开的坎。社区里 Provider、Riverpod、Bloc 和 GetX 各有拥趸,但对于很多页面复杂程度有限的小项目,引入这些库往往有种杀鸡用牛刀的感觉。经过一段时间实践,我发现 Flutter 自带的 ValueNotifier 配合 ValueListenableBuilder 就能解决大部分场景——零依赖、原生支持、性能可控。下面结合几个实际用例,分享我的用法和踩坑记录。
ValueNotifier 本质上是一个可监听的值容器,继承自 ChangeNotifier。修改它的 value 属性时会自动通知所有监听者。它与 ValueListenableBuilder 配合,可以让 UI 仅重建依赖该值的部分,避免 setState 那种整棵树 rebuild 的开销。基础用法一眼就能看懂:
- final counter = ValueNotifier<int>(0);
- counter.value++;
- ValueListenableBuilder<int>(
- valueListenable: counter,
- builder: (context, value, child) {
- return Text('$value');
- },
- );
复制代码
五个实战场景串讲
1. 基础计数
最直接的入门示例:一个按钮加减数字,只更新 Text 组件。
- final _count = ValueNotifier<int>(0);
- // 加一或减一
- _count.value++;
- _count.value--;
- Widget buildCounterUI() {
- return ValueListenableBuilder<int>(
- valueListenable: _count,
- builder: (context, value, child) {
- return Column(
- children: [
- Text('计数值: $value', style: const TextStyle(fontSize: 28)),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.tonal(
- onPressed: () => _count.value--,
- child: const Text('-1'),
- ),
- const SizedBox(width: 16),
- FilledButton(
- onPressed: () => _count.value++,
- child: const Text('+1'),
- ),
- ],
- ),
- ],
- );
- },
- );
- }
复制代码
2. 消息联动预览
输入框实时预览内容,不用 TextEditingController 监听再手动 setState。
- final lmessage = ValueNotifier<String>('初始消息');
- TextField(
- decoration: const InputDecoration(
- labelText: '输入内容',
- border: OutlineInputBorder(),
- ),
- onChanged: (v) => lmessage.value = v.isEmpty ? '(空)' : v,
- ),
- ValueListenableBuilder<String>(
- valueListenable: lmessage,
- builder: (context, value, child) {
- return Container(
- width: double.infinity,
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: Colors.blue.shade50,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text('实时预览: $value',
- style: TextStyle(color: Colors.blue.shade700)),
- );
- },
- );
复制代码
3. 待办列表
用 List<String> 作为泛型时有个关键陷阱:ValueNotifier 的 value setter 内部用 == 比较,而 List 的 == 是引用比较,不是内容比较。因此修改列表时必须创建新对象,否则不会触发 UI 更新。
- final _todoItems = ValueNotifier<List<String>>(['学习 Flutter', '写 demo', '整理文章']);
- // 新增
- _todoItems.value = [..._todoItems.value, '新待办'];
- // 删除指定项
- _todoItems.value = List.from(items)..removeAt(index);
- // 错误写法(不会触发更新)
- _todoItems.value.add(newItem);
- _todoItems.value = _todoItems.value; // 引用没变
复制代码
4. 多个 ValueNotifier 联动
主题切换与滑块数值联动,通过嵌套 ValueListenableBuilder 实现。虽然嵌套两层可读性尚可,超过三层建议抽组件或用 Provider。
- final _theme = ValueNotifier<bool>(false);
- final _sliderValue = ValueNotifier<double>(0.5);
- ValueListenableBuilder<bool>(
- valueListenable: _theme,
- builder: (context, isDark, child) {
- return ValueListenableBuilder<double>(
- valueListenable: _sliderValue,
- builder: (context, slider, child) {
- return Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: isDark ? Colors.grey.shade800 : Colors.white,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- children: [
- Text('主题: ${isDark ? "深色" : "浅色"}'),
- Slider(value: slider, onChanged: (v) => _sliderValue.value = v),
- Text('数值: ${(slider * 100).toInt()}%'),
- FilledButton.tonal(
- onPressed: () => _theme.value = !_theme.value,
- child: Text('切换 ${isDark ? "浅色" : "深色"}'),
- ),
- ],
- ),
- );
- },
- );
- },
- );
复制代码
5. 操作日志
用 ValueNotifier 记录用户每一步操作,带时间戳,自动刷新 UI。调试阶段尤其好用。
- final _logs = ValueNotifier<List<String>>([]);
- void _addLog(String msg) {
- _logs.value = [
- ..._logs.value,
- '${DateTime.now().hour}:${DateTime.now().minute}:${DateTime.now().second} $msg'
- ];
- }
- ValueListenableBuilder<List<String>>(
- valueListenable: _logs,
- builder: (context, logs, child) {
- if (logs.isEmpty) return const Text('暂无操作');
- return Column(
- children: logs.reversed.take(10).map((log) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 4),
- child: Row(
- children: [
- Icon(Icons.circle, size: 6, color: Colors.grey.shade400),
- const SizedBox(width: 8),
- Text(log, style: const TextStyle(fontSize: 12, fontFamily: 'monospace')),
- ],
- ),
- );
- }).toList(),
- );
- },
- );
复制代码
与 setState / Provider / ArkTS 的对比
setState 在简单场景下也能工作,但页面复杂后全量 rebuild 会带来性能问题,我曾遇到点击按钮整页闪烁,换成 ValueNotifier 后明显改善。
Provider 内部实际上也大量使用 ChangeNotifier,它提供了跨页面共享、依赖注入、自动 dispose 等高级能力。而 ValueNotifier 的优势是零依赖且轻量,缺点是需要自行处理跨页面共享(比如向上传递或全局单例)。建议:状态仅限当前页面或父子组件之间,用 ValueNotifier;需要跨多页面共享时,上 Provider。
鸿蒙 ArkTS 的 @State / @Prop 装饰器提供了更隐式的响应式设计——开发者只需声明状态、框架自动追踪依赖。相比之下 Flutter 的显式 builder 让精确控制更透明,但写起来多一些样板。两者设计哲学不同,无绝对优劣。
踩坑与最佳实践
- dispose 不能忘:ValueNotifier 内部维护监听者列表,务必在 dispose 中逐个 dispose。推荐用一个 List 统一管理:
- final _notifiers = <ValueNotifier>[];
- @override
- void dispose() {
- for (final n in _notifiers) {
- n.dispose();
- }
- super.dispose();
- }
复制代码
- List 对象必须引用更新:永远不要修改原列表再赋值回 value,必须创建新对象(展开运算符、List.from、copy 等)。
- 不要在 build 方法里创建 ValueNotifier:应在 initState 或成员变量声明时创建,否则每次 rebuild 都会重新创建并丢失监听者。
总结
ValueNotifier + ValueListenableBuilder 是鸿蒙 Flutter 项目中小型状态管理的轻量方案,无额外依赖、概念简单。适用场景:单页面、父子组件状态联动、日志记录、输入预览等。当页面复杂度上升或需要跨页面共享时,再升级到 Provider 即可。这套方案已在我的项目中稳定运行,如果你也在鸿蒙上使用 Flutter,不妨一试。 |