Flutter滚动短列表并关闭键盘时出现Build scheduled during frame错误
错误根源分析
这个「Build scheduled during frame」错误的核心原因是:键盘收起触发页面布局变化时,AnimatedList内部的动画回调正好在当前构建/布局帧中触发了setState,加上快速重复触摸导致的频繁状态变更,最终引发帧内构建冲突。原代码存在两个关键问题:
- 每次触摸抬起都无条件调用
unfocus,频繁触发不必要的布局变更; - AnimatedList的
itemBuilder未使用传入的animation参数,导致内部动画状态变化直接触发组件build,而非通过动画帧处理。
彻底解决的方案步骤
1. 用FocusNode精准控制文本焦点
替换全局FocusManager的方式,通过FocusNode直接绑定TextFormField,避免无效的焦点操作:
late FocusNode _textFocusNode; @override void initState() { super.initState(); _scrollController = ScrollController(initialScrollOffset: 0); _textFocusNode = FocusNode(); } @override void dispose() { _scrollController.dispose(); _textFocusNode.dispose(); super.dispose(); }
修改TextFormField绑定焦点:
TextFormField( focusNode: _textFocusNode, ),
2. 优化unfocus的触发时机
仅在焦点存在时执行unfocus,并且用addPostFrameCallback确保操作在当前帧结束后执行,避开布局/构建帧:
onPointerUp: (_) { if (_textFocusNode.hasFocus) { SchedulerBinding.instance.addPostFrameCallback((_) { _textFocusNode.unfocus(); }); } },
3. 正确使用AnimatedList的动画参数
将列表项用animation包裹,让动画变化在动画帧中处理,而非直接触发组件build:
itemBuilder: (context, index, animation) { return FadeTransition( opacity: animation, child: _buildItemWidget(_items[index]), ); },
如果不需要淡入动画,也可以用SizeTransition或AnimatedBuilder,核心是让动画状态变化通过动画系统处理,而非直接触发组件重建。
4. 实现滚动状态的动态控制
根据焦点状态设置AnimatedList的滚动物理特性,满足「键盘打开时可滚动,关闭时不可滚动」的需求:
AnimatedList( // 其他参数保持不变 physics: _textFocusNode.hasFocus ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(), ),
完整修改后的代码
class _MyHomePageState extends State<MyHomePage> { static const _durationMin = Duration.zero; final List<String> _items = List.generate(7, (index) => "Item at $index"); final GlobalKey<AnimatedListState> _listKey = GlobalKey(); late ScrollController _scrollController; late FocusNode _textFocusNode; @override void initState() { super.initState(); _scrollController = ScrollController(initialScrollOffset: 0); _textFocusNode = FocusNode(); } @override void dispose() { _scrollController.dispose(); _textFocusNode.dispose(); super.dispose(); } Widget _buildItemWidget(String text) { return ListTile( title: Text(text), trailing: IconButton( icon: const Icon(Icons.remove), onPressed: () {}, ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: SafeArea( child: Column( children: [ TextFormField( focusNode: _textFocusNode, ), Expanded( child: Listener( onPointerUp: (_) { if (_textFocusNode.hasFocus) { SchedulerBinding.instance.addPostFrameCallback((_) { _textFocusNode.unfocus(); }); } }, child: AnimatedList( key: _listKey, padding: EdgeInsets.zero, controller: _scrollController, initialItemCount: _items.length, physics: _textFocusNode.hasFocus ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(), itemBuilder: (context, index, animation) { return FadeTransition( opacity: animation, child: _buildItemWidget(_items[index]), ); }, ), ), ), Container(height: 100, color: Colors.red), ], ), ), ); } }
内容的提问来源于stack exchange,提问作者matteoh
相关产品推荐
相关产品推荐

