Flutter:拉取服务端数据时显示进度指示器 解决无限加载问题
问题根因
无限加载是FutureBuilder错误用法搭配setState触发重建形成死循环导致的,执行链路如下:
- 直接在
FutureBuilder的future参数位置调用getQoutes(),每次build方法执行时,都会重新触发一次getQoutes异步请求 - getQoutes请求完成后会调用
setState(),而setState会强制当前页面重新执行build方法 - 上述逻辑无限循环,FutureBuilder永远在等待新的Future执行完成,始终停留在加载态,加载指示器自然不会消失
修复方案
二选一即可,不要混用两种逻辑。
方案1:遵循FutureBuilder设计规范,移除多余setState
FutureBuilder本身会监听传入的Future状态自动刷新UI,不需要额外调用setState存数据,核心修改点:
- 不要在异步请求方法里调用setState,直接将请求到的数据作为返回值返回
- 不要在build方法内直接执行异步方法赋值给future,要在
initState里提前初始化、存储Future实例,避免重建时重复触发请求 - 直接从AsyncSnapshot中取数据渲染UI
修改后的核心代码:
late Future<Map<String, dynamic>> _quotesFuture; Future<Map<String, dynamic>> getQoutes() async { final data = await FirebaseFirestore.instance .collection('user') .doc(auth.currentUser!.uid) .get(); return data.data() as Map<String, dynamic>; } @override void initState() { super.initState(); _quotesFuture = getQoutes(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Some Quotes'), backgroundColor: Colors.deepOrange, ), body: FutureBuilder<Map<String, dynamic>>( future: _quotesFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } final userInfo = snapshot.data!; return SingleChildScrollView( child: Container( height: MediaQuery.of(context).size.height, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: ListView.builder( itemCount: 3, scrollDirection: Axis.vertical, itemBuilder: (context, index) { return Card_fun(userInfo['quotes'][index]); }, ), ) ], ), ), ); }, ), ); }
方案2:放弃FutureBuilder,手动管理加载状态
如果习惯用setState管理页面状态,就完全移除FutureBuilder,自行维护加载状态标记:
Map<String, dynamic> userInfo = {}; bool _isLoading = true; dynamic _loadError; Future<void> getQoutes() async { try { final data = await FirebaseFirestore.instance .collection('user') .doc(auth.currentUser!.uid) .get(); setState(() { userInfo = data.data() as Map<String, dynamic>; _isLoading = false; }); } catch (e) { setState(() { _loadError = e; _isLoading = false; }); } } @override void initState() { super.initState(); getQoutes(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Some Quotes'), backgroundColor: Colors.deepOrange, ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _loadError != null ? Text('Error: $_loadError') : SingleChildScrollView( child: Container( height: MediaQuery.of(context).size.height, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: ListView.builder( itemCount: 3, scrollDirection: Axis.vertical, itemBuilder: (context, index) { return Card_fun(userInfo['quotes'][index]); }, ), ) ], ), ), ), ); }
避坑提醒
- 禁止直接在
FutureBuilder的future参数位置执行返回Future的方法,否则只要触发build(键盘弹出、屏幕旋转、父组件刷新、setState调用),异步方法就会重复执行,轻则浪费请求资源,重则触发死循环 - FutureBuilder本身已经接管了异步状态的UI更新逻辑,不要在其传入的Future方法里额外调用setState,避免重复触发重建
内容的提问来源于stack exchange,提问作者Gireesh
相关产品推荐
相关产品推荐

