Flutter SSE 流式接收完全指南:从原理到实战

前言

在当今的移动应用开发中,实时数据推送已成为标配需求。无论是 AI 对话的逐字输出、股票行情的实时更新,还是新闻推送的即时通知,Server-Sent Events(SSE)都提供了一种比 WebSocket 更轻量、更易实现的解决方案。

本文将带领你从零开始,全面掌握 Flutter 中 SSE 的开发技巧。我们将从 SSE 协议原理讲起,逐步深入到实战代码,并最终实现一个完整的 AI 流式对话应用。


一、SSE 基础:什么是 Server-Sent Events?

1.1 SSE 的核心概念

Server-Sent Events 是一种基于 HTTP 的服务端推送技术,允许服务端主动向客户端推送数据。与传统的请求-响应模式不同,SSE 会维持一个长连接,服务端可以持续不断地发送数据。

核心特点:

· 基于 HTTP/HTTPS 协议,无需特殊协议

· 单向通信(服务端 → 客户端)

· 自动重连机制

· 轻量级,实现简单

1.2 SSE vs WebSocket:如何选择?

特性 SSE WebSocket

通信方向 单向(服务端→客户端) 双向

协议 HTTP/HTTPS WS/WSS

自动重连 ✅ 内置支持 ❌ 需手动实现

数据格式 文本(纯文本/JSON) 文本/二进制

实现复杂度 简单 较复杂

适用场景 实时通知、流式输出 聊天、游戏、双向交互

浏览器支持 所有现代浏览器 所有现代浏览器

选择建议:

· 只需服务端推送 → 选择 SSE

· 需要双向通信 → 选择 WebSocket

· 需要发送二进制数据 → 选择 WebSocket

1.3 SSE 数据格式详解

SSE 的数据格式非常简洁,每个事件由以下字段组成:

复制代码
event: message
data: {"text": "Hello"}

event: done
data: [DONE]

字段说明:

· event: - 事件类型(可选,默认 "message")

· data: - 事件数据(必需)

· id: - 事件 ID,用于断点续传(可选)

· retry: - 重连时间(毫秒,可选)

· 空行 - 表示一个事件结束

多行数据示例:

复制代码
data: {"id": 1,
data: "name": "张三",
data: "age": 25}

二、环境搭建与依赖配置

2.1 创建 Flutter 项目

bash 复制代码
flutter create flutter_sse_demo
cd flutter_sse_demo

2.2 添加必要依赖

在 pubspec.yaml 中添加依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  
  # 网络请求
  dio: ^5.4.0
  
  # 图片缓存(可选)
  cached_network_image: ^3.3.0
  
  # 状态管理
  provider: ^6.1.1
  
  # 日志打印
  logger: ^2.0.1

安装依赖:

bash 复制代码
flutter pub get

2.3 添加网络权限

对于 Android,在 android/app/src/main/AndroidManifest.xml 中添加:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />

对于 iOS,默认已支持网络访问。


三、手写实现:深入理解 SSE 底层

在引入框架之前,让我们先手写一个 SSE 客户端。这不仅能帮助我们理解协议细节,还能在特殊场景下进行定制化开发。

3.1 基础版本:使用 Dio 流式解析

dart 复制代码
// services/sse_manual_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';

class SseManualService {
  final Dio _dio = Dio();
  CancelToken? _cancelToken;
  
  /// 手动实现 SSE 流式接收
  Stream<SseEvent> connect({
    required String url,
    required Map<String, String> headers,
    Map<String, dynamic>? body,
  }) async* {
    _cancelToken = CancelToken();
    
    try {
      // 发起流式请求
      final response = await _dio.post<ResponseBody>(
        url,
        data: body,
        options: Options(
          responseType: ResponseType.stream,
          headers: {
            'Accept': 'text/event-stream',
            'Cache-Control': 'no-cache',
            ...headers,
          },
        ),
        cancelToken: _cancelToken,
      );
      
      final stream = response.data?.stream;
      if (stream == null) return;
      
      String buffer = '';  // 数据缓冲区
      
      // 逐块读取数据
      await for (final bytes in stream) {
        buffer += utf8.decode(bytes);
        
        // 按行处理,确保完整解析
        while (buffer.contains('\n')) {
          final idx = buffer.indexOf('\n');
          final line = buffer.substring(0, idx).trim();
          buffer = buffer.substring(idx + 1);
          
          if (line.isEmpty) continue;
          
          // 解析 SSE 数据行
          if (line.startsWith('data:')) {
            final dataStr = line.substring(5).trim();
            
            // 检查结束标志
            if (dataStr == '[DONE]') {
              return;
            }
            
            try {
              // 尝试解析 JSON
              final json = jsonDecode(dataStr);
              final event = SseEvent.fromJson(json);
              yield event;
            } catch (e) {
              // 非 JSON 数据,直接返回文本
              yield SseEvent(
                event: 'message',
                data: dataStr,
              );
            }
          }
        }
      }
    } catch (e) {
      if (e is DioError && CancelToken.isCancel(e)) {
        print('SSE 连接已取消');
      } else {
        print('SSE 错误: $e');
        rethrow;
      }
    }
  }
  
  /// 取消连接
  void cancel() {
    _cancelToken?.cancel('用户取消');
  }
}

/// SSE 事件模型
class SseEvent {
  final String event;
  final dynamic data;
  final String? id;
  
  SseEvent({
    required this.event,
    required this.data,
    this.id,
  });
  
  factory SseEvent.fromJson(Map<String, dynamic> json) {
    return SseEvent(
      event: json['event'] ?? 'message',
      data: json,
      id: json['id'],
    );
  }
}

3.2 核心原理解析

为什么需要 Buffer?

网络传输中,一个数据块可能不是完整的一行,甚至可能把 data: 的 JSON 从中间截断:

dart 复制代码
// 错误示例:直接解析每个 chunk
await for (final bytes in stream) {
  final line = utf8.decode(bytes);
  if (line.startsWith('data:')) { ... }  // 可能解析失败!
}

// 正确做法:使用 buffer 累积数据
String buffer = '';
await for (final bytes in stream) {
  buffer += utf8.decode(bytes);
  while (buffer.contains('\n')) {
    // 按行处理完整数据
  }
}

处理多种 SSE 字段:

dart 复制代码
String? eventType;
String? eventId;
String? eventData;

while (buffer.contains('\n')) {
  final line = ...;
  
  if (line.startsWith('event:')) {
    eventType = line.substring(6).trim();
  } 
  else if (line.startsWith('id:')) {
    eventId = line.substring(3).trim();
  } 
  else if (line.startsWith('data:')) {
    eventData = line.substring(5).trim();
  } 
  else if (line.isEmpty) {
    // 空行表示事件结束,触发回调
    if (eventData != null) {
      yield SseEvent(
        event: eventType ?? 'message',
        data: eventData,
        id: eventId,
      );
      eventData = null;
      eventType = null;
    }
  }
}

四、使用成熟框架:简化开发

手写实现虽然灵活,但需要考虑各种边界情况。使用成熟的框架可以大大提高开发效率。

4.1 方案一:dart_http_sse

这是一个轻量级的 SSE 客户端库,支持自动重连。

安装依赖:

yaml 复制代码
dependencies:
  flutter_http_sse:
    git:
      url: https://github.com/ElshiatyTube/flutter_http_sse.git

基础用法:

dart 复制代码
import 'package:flutter_http_sse/model/sse_request.dart';
import 'package:flutter_http_sse/service/sse_client.dart';

class SseSimpleService {
  final sseClient = SSEClient();
  
  Stream<SseEvent> connect(String url, Map<String, String> headers) {
    final request = SSERequest(
      requestType: SSERequestType.GET,
      url: Uri.parse(url),
      headers: headers,
      onData: (data) => print("Received: $data"),
      onError: (error) => print("Error: $error"),
      onDone: () => print("Stream closed"),
      retry: true,  // 启用自动重连
    );
    
    final Stream<SSEResponse> stream = sseClient.connect("connectionId", request);
    
    return stream.map((response) => SseEvent(
      event: response.event ?? 'message',
      data: response.data,
    ));
  }
  
  void disconnect() {
    sseClient.close(connectionId: "connectionId");
  }
}

4.2 方案二:sse_processor(推荐)

sse_processor 是一个功能强大的 SSE 处理库,特别适合处理 AI 流式响应等复杂场景。

安装依赖:

yaml 复制代码
dependencies:
  sse_processor: ^1.0.0
  dio: ^5.0.0

初始化配置:

dart 复制代码
// main.dart
import 'package:sse_processor/sse.dart';

void main() {
  final dio = Dio();
  
  SSEProcessor.init(
    SSEProcessorConfig(
      version: '1.0.0',
      debug: true,
      idleTimeout: 30.0,        // 空闲超时
      exceptionTimeout: 60,      // 异常超时
      sseBufferExtractInterval: 100, // 缓冲区提取间隔(毫秒)
    ),
    dio,
  );
  
  runApp(MyApp());
}

完整使用示例:

dart 复制代码
// services/sse_advanced_service.dart
import 'package:sse_processor/sse.dart';

class SseAdvancedService {
  final Map<String, StreamController<SseEvent>> _controllers = {};
  
  /// 创建 SSE 连接
  Stream<SseEvent> connect(String connectionId, String url, Map<String, String> headers) {
    final controller = StreamController<SseEvent>.broadcast();
    _controllers[connectionId] = controller;
    
    // 添加连接状态监听
    SSEProcessor.addConnectStateObserver(
      SSEConnectObserver(
        connectionId,
        NotifyPriority.high,
        (ConnectState state) {
          print('连接状态: $state');
          return false;
        },
      ),
    );
    
    // 添加事件拦截器
    final interceptor = SSEInterceptor(
      watchEvents: {'message', 'text', 'done'},
      autoClean: true,
      intercept: (ServerSentEvent sse, SSEChain chain) {
        final event = SseEvent(
          event: sse.elementType ?? 'message',
          data: sse.result,
          id: sse.id,
        );
        controller.add(event);
        return chain.proceed(sse);
      },
    );
    
    SSEProcessor.addSSEInterceptor(interceptor);
    
    return controller.stream;
  }
  
  /// 关闭连接
  void disconnect(String connectionId) {
    _controllers[connectionId]?.close();
    _controllers.remove(connectionId);
    SSEProcessor.removeSSEInterceptorById(connectionId);
  }
  
  /// 暂停/恢复数据分发
  void setDeliverState(String connectionId, DelivererState state) {
    SSEProcessor.setDeliverState(state);
  }
}

五、实战:完整的 AI 流式对话应用

结合前面的知识,我们来实现一个完整的 AI 流式对话应用。这个应用将支持:

· 实时流式文本输出

· 图片生成与展示

· 消息历史记录

· 优雅的 UI 反馈

5.1 项目结构

复制代码
lib/
├── models/
│   └── chat_message.dart          # 消息模型
├── services/
│   └── openai_sse_service.dart    # SSE 服务
├── viewmodels/
│   └── chat_viewmodel.dart        # 视图模型
├── widgets/
│   ├── message_bubble.dart        # 消息气泡组件
│   └── input_area.dart            # 输入区域组件
└── screens/
    └── chat_screen.dart           # 聊天界面

5.2 数据模型定义

dart 复制代码
// models/chat_message.dart
class ChatMessage {
  final String id;
  final String content;
  final bool isUser;
  final DateTime timestamp;
  final String? imageUrl;
  final bool isError;
  final bool isStreaming;
  
  ChatMessage({
    String? id,
    required this.content,
    required this.isUser,
    DateTime? timestamp,
    this.imageUrl,
    this.isError = false,
    this.isStreaming = false,
  })  : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
        timestamp = timestamp ?? DateTime.now();
  
  ChatMessage copyWith({
    String? content,
    bool? isStreaming,
    String? imageUrl,
  }) {
    return ChatMessage(
      id: id,
      content: content ?? this.content,
      isUser: isUser,
      timestamp: timestamp,
      imageUrl: imageUrl ?? this.imageUrl,
      isError: isError,
      isStreaming: isStreaming ?? this.isStreaming,
    );
  }
}

// models/sse_event.dart
class SseEvent {
  final String event;
  final Map<String, dynamic>? data;
  final String? content;
  final Map<String, dynamic>? imageData;
  
  SseEvent({
    required this.event,
    this.data,
    this.content,
    this.imageData,
  });
  
  factory SseEvent.fromJson(Map<String, dynamic> json) {
    // 解析 AI 消息内容
    String? content;
    Map<String, dynamic>? imageData;
    
    if (json['aiMessage'] != null) {
      final aiMessage = json['aiMessage'];
      content = aiMessage['content'];
      
      // 提取图片数据
      if (aiMessage['image'] != null && aiMessage['image'] is Map) {
        imageData = Map<String, dynamic>.from(aiMessage['image']);
      }
    }
    
    return SseEvent(
      event: json['event'] ?? 'message',
      data: json,
      content: content,
      imageData: imageData,
    );
  }
}

5.3 SSE 服务实现

dart 复制代码
// services/openai_sse_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';

class OpenAISseService {
  final Dio _dio = Dio();
  CancelToken? _cancelToken;
  
  /// 发送消息并获取流式响应
  Stream<SseEvent> streamChat({
    required String apiKey,
    required int conversationId,
    required String message,
    List<Map<String, String>>? history,
  }) async* {
    _cancelToken = CancelToken();
    
    try {
      final response = await _dio.post<ResponseBody>(
        'https://your-api.com/chat/stream',
        data: {
          'conversationId': conversationId,
          'content': message,
          'history': history ?? [],
          'stream': true,
        },
        options: Options(
          responseType: ResponseType.stream,
          headers: {
            'Accept': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Authorization': 'Bearer $apiKey',
            'Content-Type': 'application/json',
          },
        ),
        cancelToken: _cancelToken,
      );
      
      final stream = response.data?.stream;
      if (stream == null) return;
      
      String buffer = '';
      
      await for (final bytes in stream) {
        buffer += utf8.decode(bytes);
        
        // 按行处理
        while (buffer.contains('\n')) {
          final idx = buffer.indexOf('\n');
          final line = buffer.substring(0, idx).trim();
          buffer = buffer.substring(idx + 1);
          
          if (line.isEmpty) continue;
          
          // 解析 SSE 数据
          if (line.startsWith('data:')) {
            final dataStr = line.substring(5).trim();
            
            if (dataStr == '[DONE]') {
              return;
            }
            
            try {
              final json = jsonDecode(dataStr);
              final event = SseEvent.fromJson(json);
              yield event;
              
              // 如果是 done 事件,结束流
              if (event.event == 'done') {
                return;
              }
            } catch (e) {
              print('JSON 解析错误: $e');
            }
          }
        }
      }
    } catch (e) {
      if (e is DioError && CancelToken.isCancel(e)) {
        print('请求已取消');
      } else {
        print('SSE 错误: $e');
        rethrow;
      }
    }
  }
  
  /// 取消当前请求
  void cancel() {
    _cancelToken?.cancel('用户取消');
  }
}

5.4 ViewModel 实现

dart 复制代码
// viewmodels/chat_viewmodel.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/chat_message.dart';
import '../models/sse_event.dart';
import '../services/openai_sse_service.dart';

class ChatViewModel extends ChangeNotifier {
  final OpenAISseService _service = OpenAISseService();
  final String apiKey;
  final int conversationId;
  
  List<ChatMessage> _messages = [];
  String _currentStreamingText = '';
  String? _currentImageUrl;
  bool _isStreaming = false;
  bool _isLoading = false;
  String? _error;
  
  ChatViewModel({
    required this.apiKey,
    required this.conversationId,
  });
  
  // Getters
  List<ChatMessage> get messages => _messages;
  String get currentStreamingText => _currentStreamingText;
  String? get currentImageUrl => _currentImageUrl;
  bool get isStreaming => _isStreaming;
  bool get isLoading => _isLoading;
  String? get error => _error;
  
  /// 发送消息
  Future<void> sendMessage(String content) async {
    if (content.isEmpty) return;
    
    // 重置状态
    _error = null;
    _isLoading = true;
    _currentStreamingText = '';
    _currentImageUrl = null;
    _isStreaming = true;
    notifyListeners();
    
    // 添加用户消息
    final userMessage = ChatMessage(
      content: content,
      isUser: true,
    );
    _messages.add(userMessage);
    notifyListeners();
    
    // 准备消息历史
    final history = _messages
        .where((msg) => !msg.isStreaming)
        .map((msg) => {
          'role': msg.isUser ? 'user' : 'assistant',
          'content': msg.content,
        })
        .toList();
    
    try {
      // 开始流式接收
      await for (final event in _service.streamChat(
        apiKey: apiKey,
        conversationId: conversationId,
        message: content,
        history: history,
      )) {
        if (event.event == 'message' && event.content != null) {
          // 更新流式文本
          _currentStreamingText = event.content!;
          notifyListeners();
        } 
        else if (event.event == 'done') {
          // 最终消息
          if (event.content != null) {
            _currentStreamingText = event.content!;
          }
          
          // 提取图片
          if (event.imageData != null && event.imageData!['url'] != null) {
            _currentImageUrl = event.imageData!['url'];
          }
          
          // 保存完整消息
          _messages.add(ChatMessage(
            content: _currentStreamingText,
            isUser: false,
            imageUrl: _currentImageUrl,
          ));
          
          // 清空临时状态
          _currentStreamingText = '';
          _currentImageUrl = null;
          _isStreaming = false;
          notifyListeners();
          break;
        }
      }
    } catch (e) {
      _error = '发送失败: $e';
      _messages.add(ChatMessage(
        content: '发送失败,请重试',
        isUser: false,
        isError: true,
      ));
    } finally {
      _isLoading = false;
      _isStreaming = false;
      notifyListeners();
    }
  }
  
  /// 取消当前流式响应
  void cancelStream() {
    _service.cancel();
    _isStreaming = false;
    _isLoading = false;
    _currentStreamingText = '';
    notifyListeners();
  }
  
  /// 清空所有消息
  void clearMessages() {
    _messages.clear();
    _currentStreamingText = '';
    _currentImageUrl = null;
    _error = null;
    notifyListeners();
  }
  
  @override
  void dispose() {
    _service.cancel();
    super.dispose();
  }
}

5.5 UI 组件实现

dart 复制代码
// widgets/message_bubble.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/chat_message.dart';

class MessageBubble extends StatelessWidget {
  final ChatMessage message;
  
  const MessageBubble({Key? key, required this.message}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.only(bottom: 8),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        constraints: const BoxConstraints(maxWidth: 280),
        decoration: BoxDecoration(
          color: _getBackgroundColor(),
          borderRadius: BorderRadius.circular(12),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              message.content,
              style: TextStyle(
                color: _getTextColor(),
              ),
            ),
            if (message.imageUrl != null) ...[
              const SizedBox(height: 8),
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: CachedNetworkImage(
                  imageUrl: message.imageUrl!,
                  placeholder: (context, url) => const SizedBox(
                    height: 100,
                    child: Center(child: CircularProgressIndicator()),
                  ),
                  errorWidget: (context, url, error) => const Icon(
                    Icons.broken_image,
                    size: 50,
                  ),
                  fit: BoxFit.cover,
                ),
              ),
            ],
            const SizedBox(height: 4),
            Text(
              _formatTime(message.timestamp),
              style: TextStyle(
                fontSize: 10,
                color: _getTimeColor(),
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  Color _getBackgroundColor() {
    if (message.isError) return Colors.red.shade100;
    if (message.isUser) return Colors.blue;
    return Colors.grey.shade300;
  }
  
  Color _getTextColor() {
    if (message.isUser) return Colors.white;
    return Colors.black87;
  }
  
  Color _getTimeColor() {
    if (message.isUser) return Colors.white70;
    return Colors.grey.shade600;
  }
  
  String _formatTime(DateTime time) {
    final now = DateTime.now();
    final difference = now.difference(time);
    
    if (difference.inMinutes < 1) {
      return '刚刚';
    } else if (difference.inHours < 1) {
      return '${difference.inMinutes}分钟前';
    } else if (difference.inDays < 1) {
      return '${difference.inHours}小时前';
    } else {
      return '${time.month}/${time.day} ${time.hour}:${time.minute.toString().padLeft(2, '0')}';
    }
  }
}
dart 复制代码
// widgets/input_area.dart
import 'package:flutter/material.dart';

class InputArea extends StatelessWidget {
  final TextEditingController controller;
  final bool isLoading;
  final bool isStreaming;
  final VoidCallback onSend;
  final VoidCallback onCancel;
  
  const InputArea({
    Key? key,
    required this.controller,
    required this.isLoading,
    required this.isStreaming,
    required this.onSend,
    required this.onCancel,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            offset: const Offset(0, -2),
            blurRadius: 4,
            color: Colors.black.withOpacity(0.05),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: controller,
              enabled: !isLoading,
              maxLines: null,
              decoration: InputDecoration(
                hintText: '输入消息...',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(24),
                ),
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 8,
                ),
              ),
              onSubmitted: (_) => onSend(),
            ),
          ),
          const SizedBox(width: 8),
          if (isStreaming)
            IconButton(
              icon: const Icon(Icons.stop, color: Colors.red),
              onPressed: onCancel,
            )
          else
            IconButton(
              icon: isLoading
                  ? const SizedBox(
                      width: 24,
                      height: 24,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.send),
              onPressed: isLoading ? null : onSend,
            ),
        ],
      ),
    );
  }
}

5.6 主界面实现

dart 复制代码
// screens/chat_screen.dart
import 'package:flutter/material.dart';
import '../viewmodels/chat_viewmodel.dart';
import '../widgets/message_bubble.dart';
import '../widgets/input_area.dart';

class ChatScreen extends StatefulWidget {
  final String apiKey;
  final int conversationId;
  
  const ChatScreen({
    Key? key,
    required this.apiKey,
    required this.conversationId,
  }) : super(key: key);
  
  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  late ChatViewModel _viewModel;
  final TextEditingController _controller = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  
  @override
  void initState() {
    super.initState();
    _viewModel = ChatViewModel(
      apiKey: widget.apiKey,
      conversationId: widget.conversationId,
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    _scrollController.dispose();
    _viewModel.dispose();
    super.dispose();
  }
  
  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AI 助手'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: () => _viewModel.clearMessages(),
          ),
        ],
      ),
      body: Column(
        children: [
          // 消息列表
          Expanded(
            child: ListenableBuilder(
              listenable: _viewModel,
              builder: (context, _) {
                _scrollToBottom();
                
                return ListView.builder(
                  controller: _scrollController,
                  padding: const EdgeInsets.all(16),
                  itemCount: _viewModel.messages.length + 
                      (_viewModel.isStreaming ? 1 : 0),
                  itemBuilder: (context, index) {
                    // 正在流式输出的消息
                    if (index == _viewModel.messages.length && 
                        _viewModel.isStreaming) {
                      return _buildStreamingMessage();
                    }
                    
                    final message = _viewModel.messages[index];
                    return MessageBubble(message: message);
                  },
                );
              },
            ),
          ),
          
          // 输入区域
          InputArea(
            controller: _controller,
            isLoading: _viewModel.isLoading,
            isStreaming: _viewModel.isStreaming,
            onSend: _handleSend,
            onCancel: _viewModel.cancelStream,
          ),
        ],
      ),
    );
  }
  
  Widget _buildStreamingMessage() {
    return Align(
      alignment: Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.only(bottom: 8),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        constraints: const BoxConstraints(maxWidth: 280),
        decoration: BoxDecoration(
          color: Colors.grey.shade300,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              _viewModel.currentStreamingText,
              style: const TextStyle(color: Colors.black87),
            ),
            if (_viewModel.currentImageUrl != null) ...[
              const SizedBox(height: 8),
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(
                  _viewModel.currentImageUrl!,
                  height: 150,
                  fit: BoxFit.cover,
                ),
              ),
            ],
            const SizedBox(height: 8),
            const SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            ),
          ],
        ),
      ),
    );
  }
  
  void _handleSend() {
    if (_controller.text.isNotEmpty) {
      _viewModel.sendMessage(_controller.text);
      _controller.clear();
    }
  }
}

5.7 应用入口

dart 复制代码
// main.dart
import 'package:flutter/material.dart';
import 'screens/chat_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter SSE Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: ChatScreen(
        apiKey: 'your-api-key-here',
        conversationId: 1,
      ),
    );
  }
}

六、高级优化技巧

6.1 自动重连机制

dart 复制代码
class AutoReconnectSseService {
  int _retryCount = 0;
  static const int maxRetries = 5;
  Timer? _reconnectTimer;
  
  Stream<SseEvent> connectWithRetry(String url, Map<String, String> headers) async* {
    while (_retryCount < maxRetries) {
      try {
        await for (final event in _connect(url, headers)) {
          // 成功连接,重置重试计数
          _retryCount = 0;
          yield event;
        }
        break;
      } catch (e) {
        _retryCount++;
        if (_retryCount >= maxRetries) {
          rethrow;
        }
        
        // 指数退避
        final delay = Duration(seconds: _retryCount * _retryCount);
        await Future.delayed(delay);
      }
    }
  }
}

6.2 心跳检测

dart 复制代码
class HeartbeatAwareService {
  Timer? _heartbeatTimer;
  DateTime _lastHeartbeat = DateTime.now();
  
  Stream<SseEvent> connectWithHeartbeat(String url) async* {
    // 启动心跳检测
    _heartbeatTimer = Timer.periodic(Duration(seconds: 30), (_) {
      final timeSinceLastHeartbeat = DateTime.now().difference(_lastHeartbeat);
      if (timeSinceLastHeartbeat > Duration(seconds: 60)) {
        print('心跳超时,断开连接');
        _heartbeatTimer?.cancel();
        // 触发重连逻辑
      }
    });
    
    await for (final event in _connect(url)) {
      if (event.event == 'heartbeat') {
        _lastHeartbeat = DateTime.now();
      }
      yield event;
    }
  }
}

6.3 断点续传

dart 复制代码
class ResumeableSseService {
  String? _lastEventId;
  final SharedPreferences _prefs;
  
  ResumeableSseService(this._prefs);
  
  Stream<SseEvent> connectWithResume(String url) async* {
    // 加载上次的 event ID
    _lastEventId = _prefs.getString('last_event_id');
    
    final headers = {
      if (_lastEventId != null) 'Last-Event-ID': _lastEventId!,
    };
    
    await for (final event in _connect(url, headers)) {
      // 保存最后收到的 ID
      if (event.id != null) {
        _lastEventId = event.id;
        await _prefs.setString('last_event_id', event.id!);
      }
      yield event;
    }
  }
}

6.4 节流与防抖

dart 复制代码
class ThrottledSseService {
  Timer? _throttleTimer;
  String _pendingData = '';
  
  Stream<String> connectWithThrottle(String url) async* {
    await for (final event in _connect(url)) {
      _pendingData += event;
      
      if (_throttleTimer == null) {
        _throttleTimer = Timer(Duration(milliseconds: 16), () {
          // 每 16ms 更新一次 UI(约 60fps)
          yield _pendingData;
          _pendingData = '';
          _throttleTimer = null;
        });
      }
    }
  }
}

七、常见问题与解决方案

7.1 连接频繁断开

问题现象:SSE 连接经常意外断开。

解决方案:

· 确保服务端有心跳机制

· 设置合理的超时时间

· 实现客户端重连逻辑

dart 复制代码
// 服务端需要定期发送心跳
// 格式:data: heartbeat\n\n

7.2 数据解析失败

问题现象:JSON 解析偶尔出错。

解决方案:使用 buffer 累积数据,确保按行处理

dart 复制代码
String buffer = '';
await for (final bytes in stream) {
  buffer += utf8.decode(bytes);
  while (buffer.contains('\n')) {
    // 确保完整的一行
    final line = buffer.substring(0, buffer.indexOf('\n'));
    buffer = buffer.substring(buffer.indexOf('\n') + 1);
    // 处理 line
  }
}

7.3 UI 卡顿

问题现象:高频数据更新导致界面卡顿。

解决方案:实现节流,控制 UI 更新频率

dart 复制代码
// 每 16ms 更新一次 UI
Timer? _updateTimer;
String _buffer = '';

void onData(String chunk) {
  _buffer += chunk;
  _updateTimer ??= Timer(Duration(milliseconds: 16), () {
    setState(() {
      text += _buffer;
      _buffer = '';
    });
    _updateTimer = null;
  });
}

7.4 内存泄漏

问题现象:页面退出后连接仍然存在。

解决方案:正确管理生命周期

dart 复制代码
class _ChatScreenState extends State<ChatScreen> {
  late ChatViewModel _viewModel;
  
  @override
  void dispose() {
    _viewModel.dispose();  // 取消连接
    super.dispose();
  }
}

7.5 网络切换问题

问题现象:WiFi 切换到 4G 时连接中断。

解决方案:监听网络状态变化并重连

dart 复制代码
import 'package:connectivity_plus/connectivity_plus.dart';

class NetworkAwareService {
  StreamSubscription? _connectivitySubscription;
  
  void init() {
    _connectivitySubscription = Connectivity().onConnectivityChanged.listen((result) {
      if (result != ConnectivityResult.none) {
        // 网络恢复,触发重连
        _reconnect();
      }
    });
  }
  
  void dispose() {
    _connectivitySubscription?.cancel();
  }
}

八、性能优化建议

8.1 使用连接池

dart 复制代码
class SseConnectionPool {
  static final Map<String, StreamController<SseEvent>> _connections = {};
  
  static Stream<SseEvent> getOrCreate(String url, Map<String, String> headers) {
    if (_connections.containsKey(url)) {
      return _connections[url]!.stream;
    }
    
    final controller = StreamController<SseEvent>.broadcast();
    _connections[url] = controller;
    
    // 启动连接...
    
    return controller.stream;
  }
}

8.2 数据压缩

对于大量数据传输,可以考虑使用 gzip 压缩:

dart 复制代码
options: Options(
  headers: {
    'Accept-Encoding': 'gzip, deflate',
  },
)

8.3 智能缓冲

dart 复制代码
class AdaptiveBuffer {
  static const int _maxBufferSize = 1024 * 1024; // 1MB
  String _buffer = '';
  
  void add(String data) {
    _buffer += data;
    
    // 防止缓冲区过大
    if (_buffer.length > _maxBufferSize) {
      _buffer = _buffer.substring(_buffer.length - _maxBufferSize);
    }
  }
}

九、总结与展望

9.1 核心要点回顾

  1. SSE 基础:理解 SSE 协议格式,知道何时使用 SSE
  2. 手写实现:掌握底层原理,学会处理数据缓冲和协议解析
  3. 框架选择:根据项目需求选择合适的 SSE 库
  4. 实战应用:实现了完整的 AI 流式对话应用
  5. 优化技巧:掌握了重连、心跳、节流等高级特性

9.2 最佳实践总结

场景 推荐方案

学习研究 手写实现,深入理解原理

简单推送 dart_http_sse

AI 流式对话 sse_processor

生产环境 手写 + 完善的重连机制

低代码开发 FlutterFlow 内置支持

9.3 未来展望

随着 AI 应用的普及,SSE 在移动端的应用会越来越广泛:

· 实时翻译:边说话边翻译

· 语音转文字:实时转写

· 视频字幕:实时生成字幕

· IoT 监控:实时传感器数据推送

掌握 SSE 技术,将为你的 Flutter 开发之路增添一项重要技能。

相关推荐
西西学代码3 小时前
Flutter---文件存储
flutter
林九生4 小时前
【Flutter】Flutter 拍照/相册选择后无法显示对话框问题解决方案
前端·javascript·flutter
●VON5 小时前
Flutter组件通信详解:父子组件交互的最佳实践
javascript·flutter·华为·交互·harmonyos·von
火柴就是我6 小时前
代码记录android怎么实现状态栏导航栏隐藏
android·flutter
weixin_443478516 小时前
FLUTTER组件学习之进度指示器
学习·flutter
始持6 小时前
第十九讲 深度布局原理与优化
前端·flutter
人月神话Lee6 小时前
一个iOS开发者对Flutter中Widget、Element和RenderObject的理解
前端·flutter·ios
始持6 小时前
第二十讲 权限与设备能力
前端·flutter