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, ); }, ), ), ), ], ), ], ), ); }, ), ); } }
关键修复说明
- 重构ConnectionState逻辑:不再将
active状态视为加载状态,优先处理错误,仅在无数据且等待时显示加载指示器,正常状态直接渲染数据 - 添加初始数据:给StreamBuilder设置
initialData,避免首次进入时的空页面闪烁 - 优化定时器管理:用变量保存Timer实例,在dispose时取消,防止内存泄漏
- 空值安全处理:在判断订单列表是否为空时,添加
?? false处理nullable字段 - 调整API调用频率:将每秒调用改为5秒一次,平衡实时性与服务器负载
内容的提问来源于stack exchange,提问作者sula7ifa
相关产品推荐
相关产品推荐

