Flutter中FutureBuilder刷新致UI全重建,如何仅重建OrderStack?
问题解决:仅刷新OrderStack组件,保留ListView滚动位置
当前使用FutureBuilder频繁刷新数据时,整个Widget树会重建,导致ListView滚动位置丢失。要实现仅原地重建OrderStack完全可行,核心是避免替换Future触发整个FutureBuilder重建,而是直接维护数据状态,只更新依赖数据的组件。
具体实现步骤
- 替换Future变量为数据状态:不再用
ordersFuture存储异步任务,直接保存接口返回的数据、加载状态和错误信息。 - 封装数据获取方法:单独写异步方法获取数据,通过
setState更新状态,而非替换Future。 - 重构UI构建逻辑:去掉FutureBuilder,直接根据当前加载状态、错误状态和数据渲染UI,仅当数据变化时,OrderStack所在的列表项才会重建,ListView的结构和滚动位置得以保留。
修改后的完整代码
class ServingStationScreen extends StatefulWidget { const ServingStationScreen({Key? key}) : super(key: key); @override ServingStationScreenState createState() => ServingStationScreenState(); } class ServingStationScreenState extends State<ServingStationScreen> { // 直接存储接口返回的数据 Map<String, List<Order>>? ordersData; // 加载状态标记 bool isLoading = true; // 错误信息存储 String? errorMessage; final List<String> emptyImages = [ 'assets/svg/barbecue.svg', 'assets/svg/beer.svg', 'assets/svg/breakfast.svg', 'assets/svg/cooking.svg', 'assets/svg/fresh-drink.svg', 'assets/svg/ice-cream.svg', 'assets/svg/mint-tea.svg', 'assets/svg/pizza.svg', 'assets/svg/special-event.svg', 'assets/svg/tea.svg', ]; @override void initState() { super.initState(); // 初始化时拉取数据 fetchOrders(); } // 封装数据获取逻辑 Future<void> fetchOrders() async { setState(() { isLoading = true; errorMessage = null; }); try { var orders = (await APIService().getOrders())!; final data = { 'ordered': orders.where((order) { return order.itemsOrdered!.isNotEmpty; }).toList(), 'preparingAndAwaitingCancellation': orders.where((order) { return order.itemsPreparingAndAwaitingCancellation!.isNotEmpty; }).toList(), }; setState(() { ordersData = data; isLoading = false; }); } catch (e) { setState(() { errorMessage = e.toString(); isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: Navigation( allowBack: false, activePage: 'Serving Station', restaurantName: 'Test Restaurant', actions: [ Padding( padding: const EdgeInsets.only(right: 10), child: IconButton( onPressed: fetchOrders, // 点击直接触发数据刷新 icon: const Icon( Icons.history_rounded, color: Colors.white, size: 26.0, ), ), ), ], ), extendBodyBehindAppBar: true, backgroundColor: const Color.fromRGBO(21, 21, 32, 1), body: buildBody(), ); } Widget buildBody() { // 加载中状态 if (isLoading) { return const CircularProgressIndicator( color: Color.fromRGBO(93, 194, 188, 1), ); } // 错误状态 if (errorMessage != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( padding: const EdgeInsets.only(bottom: 100), child: Opacity( opacity: 0.5, child: SvgPicture.asset( 'assets/svg/server.svg', height: SizerUtil.deviceType == DeviceType.tablet ? 500 : 200, theme: const SvgTheme( currentColor: Color.fromRGBO(93, 194, 188, 1), ), ), ), ), Text( 'Service Unreachable', textAlign: TextAlign.center, style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 30 : 18, color: Colors.white), ), Text( 'Error: $errorMessage', textAlign: TextAlign.center, style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 15 : 10, color: Colors.red), ), ], ), ); } // 数据为空状态 final data = ordersData!; if (data['preparingAndAwaitingCancellation']!.isEmpty && data['ordered']!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( padding: const EdgeInsets.only(bottom: 100), child: Opacity( opacity: 0.5, child: SvgPicture.asset( emptyImages.elementAt(Random().nextInt(emptyImages.length)), height: SizerUtil.deviceType == DeviceType.tablet ? 500 : 200, theme: const SvgTheme( currentColor: Color.fromRGBO(93, 194, 188, 1), ), ), ), ), Text( 'There are currently no pending orders.', textAlign: TextAlign.center, style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 30 : 18, color: Colors.white), ), ], ), ); } // 正常数据状态 return SafeArea( bottom: false, child: ListView( shrinkWrap: false, children: [ if (data['preparingAndAwaitingCancellation']!.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(top: 20, left: 20, bottom: 20), child: Row( children: [ const Text('Current Items', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 30, ), ), Container( margin: const EdgeInsets.only(left: 10, right: 10), padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2), decoration: BoxDecoration( color: Colors.yellow[200], border: Border.all( color: Colors.transparent, ), borderRadius: const BorderRadius.all(Radius.circular(20)) ), child: const Text( 'Preparing', style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), Container( padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2), decoration: BoxDecoration( color: Colors.red[200], border: Border.all( color: Colors.transparent, ), borderRadius: const BorderRadius.all(Radius.circular(20)) ), child: const Text( 'Awaiting Cancellation', style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), ], ), ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: data['preparingAndAwaitingCancellation']!.length * 500 < 2500 ? data['preparingAndAwaitingCancellation']!.length * 500 : 2500, child: MasonryGridView.count( physics: const NeverScrollableScrollPhysics(), crossAxisCount: data['preparingAndAwaitingCancellation']!.length < 5 ? data['preparingAndAwaitingCancellation']!.length : 5, mainAxisSpacing: 10, crossAxisSpacing: 10, shrinkWrap: true, itemCount: data['preparingAndAwaitingCancellation']!.length, padding: const EdgeInsets.only(left: 20, right: 20), itemBuilder: (context, index) { return OrderStack( order: data['preparingAndAwaitingCancellation']![index], filter: OrderStatusFilter.preparingAndAwaitingCancellation, ); }, ), ), ), ], ), if (data['ordered']!.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(top: 20, left: 20, bottom: 20), child: Row( children: [ const Text('New Items', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 30, ), ), Container( margin: const EdgeInsets.only(left: 10, right: 10), padding: const EdgeInsets.only(left: 5, right: 5, top: 2, bottom: 2), decoration: BoxDecoration( color: Colors.white, border: Border.all( color: Colors.transparent, ), borderRadius: const BorderRadius.all(Radius.circular(20)) ), child: const Text( 'Ordered', style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), ], ), ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: data['ordered']!.length * 500 < 2500 ? data['ordered']!.length * 500 : 2500, child: MasonryGridView.count( physics: const NeverScrollableScrollPhysics(), crossAxisCount: data['ordered']!.length < 5 ? data['ordered']!.length : 5, mainAxisSpacing: 10, crossAxisSpacing: 10, shrinkWrap: true, itemCount: data['ordered']!.length, padding: const EdgeInsets.only(left: 20, right: 20), itemBuilder: (context, index) { return OrderStack( order: data['ordered']![index], filter: OrderStatusFilter.ordered, ); }, ), ), ), ], ), ], ), ); } }
原理说明
- 直接维护
ordersData、isLoading、errorMessage状态,调用fetchOrders()时仅更新这些状态,不会触发整个ListView重建,仅更新依赖数据的OrderStack组件。 - ListView的结构保持不变,滚动位置会被自动保留,不会重置。
- 相比FutureBuilder方案,这种方式能精准控制UI更新范围,避免不必要的Widget重建,性能更优。
内容的提问来源于stack exchange,提问作者sula7ifa
相关产品推荐
相关产品推荐

