Flutter macOS端build()触发但界面未更新,仅热重载后生效?
问题:Flutter桌面应用动态更新列表无界面变化,仅热重载生效
我有一个每秒生成连续整数的异步流:
Stream<int> f() async* { for (int i = 0; i < 100; ++i) { await Future.delayed(Duration(seconds: 1)); yield i; } }
定义状态变量List<Widget> l = [];,希望通过该流每秒向列表中添加一个Card组件:
@override void initState() { super.initState(); f().listen((d) { setState( () => this.l.add(Card(child: ListTile(title: Text(d.toString()))))); }); }
对应的build()方法如下:
@override Widget build(BuildContext context) { print('build(): ${this.l.length}'); return ListView(children: this.l); }
通过flutter run -d macos运行macOS桌面应用时,尽管build()每秒都会被调用,且this.l持续更新,但界面完全没有变化,控制台输出如下:
flutter: build(): 0 flutter: build(): 1 flutter: build(): 2 flutter: build(): 3 flutter: build(): 4 flutter: build(): 5 ...
只有按下r键执行热重载后,界面才会更新。
复现步骤:
- 执行
flutter run -d macos启动应用; - 尽管
build()每秒触发,界面仍保持全黑状态; - 按下r键执行热重载,界面更新。
完整代码:
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.blueGrey, useMaterial3: true), home: Scaffold(body: W()), debugShowCheckedModeBanner: false, ); } } class W extends StatefulWidget { const W({ super.key, }); @override State<W> createState() => _WState(); } class _WState extends State<W> { List<Widget> l = []; @override void initState() { super.initState(); Stream<int> f() async* { for (int i = 0; i < 100; ++i) { await Future.delayed(Duration(seconds: 1)); yield i; } } f().listen((d) { setState( () => this.l.add(Card(child: ListTile(title: Text(d.toString()))))); }); } @override Widget build(BuildContext context) { print('build(): ${this.l.length}'); return ListView(children: this.l); } }
原因分析
问题核心在于直接修改原有列表对象的操作:this.l.add(...)只是在原列表基础上添加元素,列表本身的内存引用没有发生变化。
Flutter的setState机制通过对比状态的引用变化判断是否需要更新UI。对于List这类集合类型,Flutter会检查引用是否相等——因为始终操作同一个列表对象,setState认为状态未发生实质性变更,因此不会触发有效的UI重建。尽管build()被调用,但ListView接收的是同一个列表引用,不会重新渲染子组件列表。
热重载时Flutter会重新构建整个组件树,此时ListView会重新读取列表当前内容,界面才会更新。
解决方法
需要在setState中创建新的列表对象,让状态引用发生变化,才能让Flutter识别到更新:
方法一:复制原有元素生成新列表
setState(() { this.l = List.from(this.l) ..add(Card(child: ListTile(title: Text(d.toString())))); });
方法二:使用扩展运算符生成新列表
setState(() { this.l = [...this.l, Card(child: ListTile(title: Text(d.toString())))]; });
方法三:使用StreamBuilder(推荐的响应式写法)
无需手动维护状态列表,直接通过StreamBuilder监听流并构建UI:
class _WState extends State<W> { final List<int> _dataList = []; late StreamSubscription _subscription; Stream<int> f() async* { for (int i = 0; i < 100; ++i) { await Future.delayed(Duration(seconds: 1)); yield i; } } @override void initState() { super.initState(); _subscription = f().listen((d) { setState(() { _dataList.add(d); // 创建新列表触发UI更新 _dataList = List.from(_dataList); }); }); } @override void dispose() { _subscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { print('build(): ${_dataList.length}'); return ListView( children: _dataList.map((d) => Card(child: ListTile(title: Text(d.toString())))).toList(), ); } }
内容的提问来源于stack exchange,提问作者ynn
相关产品推荐
相关产品推荐

