You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中FutureBuilder刷新致UI全重建,如何仅重建OrderStack?

问题解决:仅刷新OrderStack组件,保留ListView滚动位置

当前使用FutureBuilder频繁刷新数据时,整个Widget树会重建,导致ListView滚动位置丢失。要实现仅原地重建OrderStack完全可行,核心是避免替换Future触发整个FutureBuilder重建,而是直接维护数据状态,只更新依赖数据的组件。

具体实现步骤

  1. 替换Future变量为数据状态:不再用ordersFuture存储异步任务,直接保存接口返回的数据、加载状态和错误信息。
  2. 封装数据获取方法:单独写异步方法获取数据,通过setState更新状态,而非替换Future。
  3. 重构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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 13:15:32