Flutter问题:后端数据更新时StreamBuilder无法自动更新ListView
问题根源
你当前的Stream只一次性请求后端数据并发送一次结果,之后不会主动监听后端的更新,所以StreamBuilder只会渲染初始数据,后端新增/修改数据后无法触发UI刷新。Firebase的实时数据库自带监听机制,但Spring Boot+MySQL需要自己实现数据变更的实时通知。
解决方案
针对你的场景,推荐两种实现方式,按上手难度排序:
方式1:定时轮询(适合新手快速实现)
通过定时重复请求后端接口,拉取最新聊天数据,让Stream持续推送新结果触发UI更新。
代码修改步骤:
- 修改ChatController的
getChatsList方法,让它持续轮询:
Stream<dynamic> getChatsList(int roomId, int categoryId) async* { // 每隔2秒请求一次后端(可根据需求调整间隔) while (true) { var result = await _chatApi.getRoomChatList(roomId, categoryId); yield result; await Future.delayed(const Duration(seconds: 2)); } }
- 在ChatScreen中复用Stream,避免重复创建:
class _ChatScreenState extends ConsumerState<ChatScreen> with SingleTickerProviderStateMixin { dynamic dataItem; List<dynamic>? chatList = []; Stream<dynamic>? _chatStream; // 新增存储Stream @override void initState() { super.initState(); // 页面初始化时创建一次Stream,避免每次build重复发起轮询 _chatStream = ref.read(chatControllerProvider).getChatsList(widget.data['id'], widget.category['id']); } @override Widget build(BuildContext context) { return Container( child: WillPopScope( child: showChat(), onWillPop: () async { // 可添加返回逻辑 return true; }, ), ); } showChat() { return StreamBuilder<dynamic>( stream: _chatStream, // 使用存储的Stream builder: (context, snapshot) { if (!snapshot.hasData) { return const Loader(); } // 修正空判断:检查chatList是否为空 else if(snapshot.data!['chatList'].isEmpty) { return NoChat(); } else { dataItem = snapshot.data!['chatRoom']; chatList = snapshot.data!['chatList']; return showChatView(); } }, ); } // ... 其余代码不变 }
方式2:WebSocket实时推送(适合生产环境,实时性更好)
通过Spring Boot实现WebSocket服务器,当数据库有新消息时主动推送给Flutter客户端,彻底解决轮询的延迟问题。
后端(Spring Boot)配置(简化示例):
- 添加WebSocket依赖到
pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
- 配置WebSocket处理器和端点:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), "/ws/chat") .setAllowedOrigins("*"); // 生产环境需配置具体域名 } } public class ChatWebSocketHandler extends TextWebSocketHandler { private static final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); // 可根据请求参数获取roomId、categoryId,关联会话 } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 处理客户端消息(如果需要) } // 当数据库有新消息时,调用此方法推送给对应房间的客户端 public static void sendMessageToRoom(String roomId, String message) throws IOException { for (WebSocketSession session : sessions) { // 判断会话所属房间,发送消息 session.sendMessage(new TextMessage(message)); } } }
- 在新增/更新聊天消息的业务逻辑中,调用
sendMessageToRoom推送最新数据。
Flutter端代码修改:
- 添加
web_socket_channel依赖到pubspec.yaml:
dependencies: web_socket_channel: ^2.4.0
- 修改ChatApi,添加WebSocket支持:
import 'package:web_socket_channel/web_socket_channel.dart'; import 'dart:convert'; class ChatApi{ late Dio dioRest; WebSocketChannel? _channel; ChatApi() { dioRest = Dio( BaseOptions( headers: { HttpHeaders.contentTypeHeader: ContentType.json.value, }, ), ); } // 初始化WebSocket连接 Future<void> initWebSocket(int roomId, int categoryId) async { final token = getToken(); // 替换为你的后端WebSocket地址,将http改为ws final wsUrl = 'ws://${token.baseUrl!.replaceFirst('http://', 'ws://')}/ws/chat?roomId=$roomId&categoryId=$categoryId'; _channel = WebSocketChannel.connect(Uri.parse(wsUrl)); } // 获取WebSocket消息流 Stream<dynamic> getWebSocketStream() { return _channel!.stream.map((event) => jsonDecode(event)); } // 关闭WebSocket连接 void closeWebSocket() { _channel?.sink.close(); } // 保留原有的历史消息查询方法 Future getRoomChatList(int roomId,int categoryId) async { try { token = getToken(); var result = await dioRest.post(token.baseUrl!+'..../getRoomChatList',options: Options(.....), queryParameters: {'roomId' : roomId,'categoryId' : categoryId}, ); if (result.statusCode == 200) { return jsonDecode(result.data); } throw DioError(requestOptions: result.requestOptions); } on DioError catch (error) { if (error.response!.statusCode! >= 400) { throw TokenException(message: "Token invalid or expired"); } throw BusinessException(BusinessError.fromJson(error.response?.data), statusCode: error.response?.statusCode); } catch (error) { throw Error(); } } }
- 修改ChatController,组合历史消息和实时消息流:
class ChatController { final ChatApi _chatApi; final ProviderRef _ref; ChatController({ required ChatApi chatApi, required ProviderRef ref, }) : _chatApi = chatApi, _ref = ref; // 先返回历史消息,再监听实时推送 Stream<dynamic> getChatsStream(int roomId,int categoryId) async* { // 先加载历史消息 final historyData = await _chatApi.getRoomChatList(roomId, categoryId); yield historyData; // 初始化WebSocket await _chatApi.initWebSocket(roomId, categoryId); // 监听实时消息 await for (final realtimeData in _chatApi.getWebSocketStream()) { yield realtimeData; } } // 页面销毁时关闭WebSocket void closeWebSocket() { _chatApi.closeWebSocket(); } }
- 修改ChatScreen使用新的Stream并管理WebSocket生命周期:
class _ChatScreenState extends ConsumerState<ChatScreen> with SingleTickerProviderStateMixin { dynamic dataItem; List<dynamic>? chatList = []; Stream<dynamic>? _chatStream; @override void initState() { super.initState(); _chatStream = ref.read(chatControllerProvider).getChatsStream(widget.data['id'], widget.category['id']); } @override void dispose() { // 页面销毁时关闭WebSocket ref.read(chatControllerProvider).closeWebSocket(); super.dispose(); } // ... 其余build、showChat、showChatView代码不变,仅将StreamBuilder的stream改为_chatStream }
内容的提问来源于stack exchange,提问作者KJEjava48
相关产品推荐
相关产品推荐

