Flutter build期间调用setState引发异常的解决问询
这个问题我之前也遇到过,核心原因是你在Flutter的构建流程执行期间触发了状态更新,咱们一步步拆解问题和解决方案:
为什么会抛出异常?
你在FutureBuilder的builder方法里直接调用了MyNotification(snapshot).dispatch(context)——而builder是当前页面构建流程的一部分,此时Flutter正在渲染组件树。通知发送后会立刻触发NotificationListener的回调,回调里的setState会尝试让组件重新构建,这就违反了Flutter的规则:不能在build过程中调用setState或标记组件需要重新构建。
快速修复当前问题
要解决这个异常,只需要把通知的发送推迟到当前帧构建完成之后,用WidgetsBinding.instance.addPostFrameCallback即可:
修改MyRemoteObjectView中FutureBuilder的builder代码:
builder: (BuildContext context, AsyncSnapshot<T> snapshot) { // 推迟到当前帧构建完成后再发送通知 WidgetsBinding.instance.addPostFrameCallback((_) { MyNotification(snapshot).dispatch(context); }); // ... 你的原有UI渲染代码 }
这样通知会在当前页面的build流程完全结束后再触发,此时调用setState就不会触发异常了。
更简便的替代实现方案
其实用Notification来传递这种父子组件间的状态有点“大材小用”,因为Notification更多适用于跨多层级组件的状态传递。对于你的场景,有两种更简洁的方案:
方案1:用回调函数传递状态
直接在MyRemoteObjectView中定义回调参数,把Snapshot状态传递给父组件:
// 修改MyRemoteObjectView,添加回调参数 class MyRemoteObjectView<T> extends StatelessWidget { final void Function(AsyncSnapshot<T>) onSnapshotUpdated; const MyRemoteObjectView({Key? key, required this.onSnapshotUpdated}) : super(key: key); Future<T> getData() { // 你的API请求逻辑 } @override Widget build(BuildContext context) { return FutureBuilder<T>( future: getData(), builder: (context, snapshot) { // 同样用postFrameCallback避免build期间触发回调 WidgetsBinding.instance.addPostFrameCallback((_) { onSnapshotUpdated(snapshot); }); // ... 原有UI渲染代码 }, ); } }
然后在父组件中使用时传入回调:
@override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar(), body: MyRemoteObjectView( onSnapshotUpdated: (snapshot) { if (snapshot.connectionState == ConnectionState.done) { setState(() { _serverHasBeenCalled = true; _modelDataRetrieved = snapshot.hasData; }); } }, ), floatingActionButton: MyFloatingButton(), ); }
方案2:状态提升(推荐)
把Future和状态逻辑直接提升到父组件的State中,让子组件只负责展示UI,这是Flutter状态管理的最佳实践之一:
class _YourPageState extends State<YourPage> { late Future<T> _dataFuture; bool _serverHasBeenCalled = false; bool _modelDataRetrieved = false; @override void initState() { super.initState(); // 初始化请求Future _dataFuture = getData(); // 监听Future的完成状态 _dataFuture.then((_) { setState(() { _serverHasBeenCalled = true; _modelDataRetrieved = true; }); }).catchError((_) { setState(() { _serverHasBeenCalled = true; _modelDataRetrieved = false; }); }); } // 把原MyRemoteObjectView中的API请求逻辑移到这里 Future<T> getData() { // 你的REST API请求代码 } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar(), // 子组件只接收Future来渲染UI body: MyRemoteObjectView(future: _dataFuture), floatingActionButton: MyFloatingButton( // 直接把状态传给悬浮按钮 serverCalled: _serverHasBeenCalled, hasData: _modelDataRetrieved, ), ); } } // 修改MyRemoteObjectView为纯展示组件 class MyRemoteObjectView<T> extends StatelessWidget { final Future<T> future; const MyRemoteObjectView({Key? key, required this.future}) : super(key: key); @override Widget build(BuildContext context) { return FutureBuilder<T>( future: future, builder: (context, snapshot) { // ... 原有UI渲染逻辑,无需发送通知 }, ); } }
这种方式把状态统一放在父组件管理,逻辑更清晰,也彻底避免了Notification和回调带来的额外复杂度。
内容的提问来源于stack exchange,提问作者daveoncode

