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

StreamBuilder未响应StreamController.sink添加的数据

问题分析与修复方案

核心问题:StreamBuilder的ConnectionState逻辑错误

你的代码中,当Stream处于ConnectionState.active状态(即数据持续流动时),返回的是加载指示器,而实际订单数据的渲染逻辑仅在ConnectionState.done分支中——但只有当StreamController被关闭时,状态才会变为done。这就导致即使数据正常发送到Stream,界面也一直显示加载,永远不会更新到订单列表。

其他潜在问题

  • streamController未设置初始数据,首次进入会先显示空页面,直到第一次API请求完成
  • 错误处理中的setState完全多余,addError会直接触发StreamBuilder的错误分支
  • 每秒调用一次API过于频繁,会造成不必要的服务器压力

修复后的完整代码

import 'dart:async';
import 'dart:math';

import 'package:dineos_tools/api/api_service.dart';
import 'package:dineos_tools/models/serving_station/order.dart';
import 'package:dineos_tools/screens/serving_station_history_screen.dart';
import 'package:dineos_tools/widgets/navigation.dart';
import 'package:dineos_tools/widgets/order_stack.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:sizer/sizer.dart';

class ServingStationScreenStreamed extends StatefulWidget {
  const ServingStationScreenStreamed({Key? key}) : super(key: key);

  @override
  ServingStationScreenState createState() => ServingStationScreenState();
}

class ServingStationScreenState extends State<ServingStationScreenStreamed> {
  late final StreamController<Map<String, List<Order>>> streamController;
  Timer? _timer;

  Future<void> getOrders() async {
    try {
      var orders = await APIService().getOrders();
      if (orders == null) {
        streamController.sink.add({
          'ordered': [],
          'preparingAndAwaitingCancellation': [],
        });
        return;
      }
      streamController.sink.add({
        'ordered': orders.where((order) {
          return order.itemsOrdered?.isNotEmpty ?? false;
        }).toList(),
        'preparingAndAwaitingCancellation': orders.where((order) {
          return order.itemsPreparingAndAwaitingCancellation?.isNotEmpty ?? false;
        }).toList(),
      });
    } catch (e) {
      streamController.sink.addError(e);
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    streamController.close();
    super.dispose();
  }
  
  @override
  void initState() {
    super.initState();
    streamController = StreamController();
    getOrders();
    // 调整为5秒一次,平衡实时性与服务器压力
    _timer = Timer.periodic(const Duration(seconds: 5), (timer) {
      getOrders();
    });
  }

  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
  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: () {
                Get.to(() => const ServingStationHistoryScreen());
              },
              icon: const Icon(
                Icons.history_rounded,
                color: Colors.white,
                size: 26.0,
              ),
            ),
          ),
        ],
      ),
      extendBodyBehindAppBar: true,
      backgroundColor: const Color.fromRGBO(21, 21, 32, 1),
      body: StreamBuilder<Map<String, List<Order>>>(
        stream: streamController.stream,
        initialData: {
          'ordered': [],
          'preparingAndAwaitingCancellation': [],
        },
        builder: (context, snapshot) {
          // 优先处理错误状态
          if (snapshot.hasError) {
            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: ${snapshot.error}',
                    textAlign: TextAlign.center,
                    style: TextStyle(fontSize: SizerUtil.deviceType == DeviceType.tablet ? 15 : 10, color: Colors.red),
                  ),
                ],
              ),
            );
          }

          // 仅在无数据且处于等待状态时显示加载
          if (snapshot.connectionState == ConnectionState.waiting && snapshot.data!.values.every((list) => list.isEmpty)) {
            return const Center(
              child: CircularProgressIndicator(
                color: Color.fromRGBO(93, 194, 188, 1),
              ),
            );
          }

          final data = snapshot.data!;
          final isEmpty = data['ordered']!.isEmpty && data['preparingAndAwaitingCancellation']!.isEmpty;
          
          // 无数据时显示空状态
          if (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(
                        restorationId: 'firstScroll',
                        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(
                        restorationId: 'secondScroll',
                        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,
                              );
                            },
                          ),
                        ),
                      ),
                    ],
                  ),
              ],
            ),
          );
        },
      ),
    );
  }
}

关键修复说明

  1. 重构ConnectionState逻辑:不再将active状态视为加载状态,优先处理错误,仅在无数据且等待时显示加载指示器,正常状态直接渲染数据
  2. 添加初始数据:给StreamBuilder设置initialData,避免首次进入时的空页面闪烁
  3. 优化定时器管理:用变量保存Timer实例,在dispose时取消,防止内存泄漏
  4. 空值安全处理:在判断订单列表是否为空时,添加?? false处理nullable字段
  5. 调整API调用频率:将每秒调用改为5秒一次,平衡实时性与服务器负载

内容的提问来源于stack exchange,提问作者sula7ifa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 20:20:18