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

Flutter问题:后端数据更新时StreamBuilder无法自动更新ListView

问题根源

你当前的Stream只一次性请求后端数据并发送一次结果,之后不会主动监听后端的更新,所以StreamBuilder只会渲染初始数据,后端新增/修改数据后无法触发UI刷新。Firebase的实时数据库自带监听机制,但Spring Boot+MySQL需要自己实现数据变更的实时通知。

解决方案

针对你的场景,推荐两种实现方式,按上手难度排序:


方式1:定时轮询(适合新手快速实现)

通过定时重复请求后端接口,拉取最新聊天数据,让Stream持续推送新结果触发UI更新。

代码修改步骤:

  1. 修改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));
  }
}
  1. 在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)配置(简化示例):

  1. 添加WebSocket依赖到pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 配置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));
        }
    }
}
  1. 在新增/更新聊天消息的业务逻辑中,调用sendMessageToRoom推送最新数据。

Flutter端代码修改:

  1. 添加web_socket_channel依赖到pubspec.yaml:
dependencies:
  web_socket_channel: ^2.4.0
  1. 修改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();
    }
  }
}
  1. 修改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();
  }
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 21:32:53