Flutter问题:FutureBuilder在build未触发时重复执行已完成Future
Flutter中FutureBuilder重复执行已完成Future但未触发build()的问题分析与解决
问题概述
已在initState()中通过AsyncMemoizer包裹创建了Future,确保Future只执行一次,但在软键盘显示/隐藏的特定场景下(使用FocusScopeNode.unfocus()时有概率触发),FutureBuilder的builder回调会重复执行已完成的Future对应的快照(ConnectionState.done),而外层State的build()方法并未被调用。这和常见的“Future在build中创建导致重复执行”的问题不同。
复现代码
简化业务代码(带AsyncMemoizer)
class _HomeScreenState extends State<HomeScreen> { late final Future myFuture; final AsyncMemoizer _memoizer = AsyncMemoizer(); @override void initState() { super.initState(); // 使用Provider获取服务 final homeService = context.read<HomeService>(); myFuture = _memoizer.runOnce(homeService.getMyData); } @override Widget build(BuildContext context) { print("[HOME] BUILDING OUR HOME SCREEN"); return FutureBuilder( future: myFuture, builder: ((context, snapshot) { print("[HOME] BUILDER CALLED WITH SNAPSHOT: $snapshot - connection state: ${snapshot.connectionState}"); // ... 后续UI逻辑 }), ); } }
完整测试代码
import 'package:flutter/material.dart'; void main() async { runApp(const TestApp()); } class TestApp extends StatelessWidget { const TestApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Testapp', home: Scaffold( body: TestAppHomeScreen(), ), ); } } class TestAppHomeScreen extends StatefulWidget { const TestAppHomeScreen({super.key}); @override State<TestAppHomeScreen> createState() => _TestAppHomeScreenState(); } class _TestAppHomeScreenState extends State<TestAppHomeScreen> { late final Future myFuture; @override void initState() { super.initState(); myFuture = Future.delayed(const Duration(milliseconds: 500), () => true); print("[HOME] HOME SCREEN INIT STATE CALLED: $hashCode"); } @override Widget build(BuildContext context) { print("[HOME] HOME SCREEN BUILD CALLED: $hashCode"); return FutureBuilder( future: myFuture, builder: (context, snapshot) { print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode"); if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } return GestureDetector( onTapUp: (details) { // 隐藏软键盘的方式会影响问题是否触发 FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } // 使用FocusManager则不会触发该问题 // FocusManager.instance.primaryFocus?.unfocus(); }, child: const Scaffold( body: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 32.0), child: TextField(), ), ), ), ); }, ); } }
问题日志示例
I/flutter (29283): [HOME] BUILDING OUR HOME SCREEN I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null, null) - connection state: ConnectionState.waiting I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done ... // 触发问题时 I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done
原因分析
- FutureBuilder内部机制:FutureBuilder会监听关联Future的状态,同时依赖上下文的InheritedWidget变化。使用
FocusScopeNode.unfocus()时,会触发Focus相关的InheritedWidget更新,FutureBuilder作为依赖该上下文的组件,会重新执行builder回调,但外层State未调用setState(),所以build()方法不会触发。 - 软键盘隐藏方式差异:
FocusScope.unfocus()会触发FocusNode状态变更,进而导致上下文的InheritedWidget更新;而FocusManager.instance.primaryFocus?.unfocus()处理更温和,不会触发这类上下文更新,因此不会触发builder重复调用。 - 已完成Future的builder触发:即使Future已经完成,当FutureBuilder的依赖发生变化时,它仍会重新调用builder并传入已完成的快照,而非重新执行Future(AsyncMemoizer已确保Future只执行一次)。
解决方案
方案1:将Future结果存储到State变量,手动控制UI更新
放弃使用FutureBuilder,直接在Future完成后调用setState()更新状态,用状态变量构建UI,从根源上避免FutureBuilder的自动回调:
class _TestAppHomeScreenState extends State<TestAppHomeScreen> { bool? _futureResult; @override void initState() { super.initState(); // 执行Future并更新状态 Future.delayed(const Duration(milliseconds: 500), () => true) .then((value) { if (mounted) { setState(() { _futureResult = value; }); } }); print("[HOME] HOME SCREEN INIT STATE CALLED: $hashCode"); } @override Widget build(BuildContext context) { print("[HOME] HOME SCREEN BUILD CALLED: $hashCode"); // 根据状态变量展示不同UI if (_futureResult == null) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } return GestureDetector( onTapUp: (details) { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: const Scaffold( body: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 32.0), child: TextField(), ), ), ), ); } }
方案2:给FutureBuilder添加固定Key
通过给FutureBuilder设置唯一Key,帮助Flutter识别组件身份,避免因上下文变化导致的不必要builder调用:
@override Widget build(BuildContext context) { print("[HOME] HOME SCREEN BUILD CALLED: $hashCode"); return FutureBuilder( key: const ValueKey('home_future_builder'), // 固定Key future: myFuture, builder: (context, snapshot) { // ... 原有builder逻辑 }, ); }
方案3:在builder中缓存逻辑结果
如果必须使用FutureBuilder,可以在builder中对计算结果进行缓存,避免重复执行耗时逻辑(比如数据解析、UI组件创建):
// 在State中定义缓存变量 Widget? _cachedContent; @override Widget build(BuildContext context) { print("[HOME] HOME SCREEN BUILD CALLED: $hashCode"); return FutureBuilder( future: myFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } // 缓存已完成状态的UI if (_cachedContent == null) { _cachedContent = GestureDetector( onTapUp: (details) { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: const Scaffold( body: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 32.0), child: TextField(), ), ), ), ); } return _cachedContent!; }, ); }
内容的提问来源于stack exchange,提问作者divillysausages
相关产品推荐
相关产品推荐

