鸿蒙应用开发实战之基于 Flutter 的 AI 聊天室实现与 Gemini API 流式集成【跨平台技术在开源鸿蒙中的使用】

随着人工智能技术的发展,AI 聊天室已经成为许多应用的重要功能模块。本文将结合 FlutterGemini API,详细介绍如何构建一个支持实时流式响应的 AI 聊天室,同时兼顾 UI 展示和复杂 JSON 数据解析的实现技巧。本文的示例将以 Web 展示为主,但同样适用于 OpenHarmony 等生态系统。


项目背景

在之前的项目中,我们已经实现了一个简易的文件管理器,并支持 Markdown 文件的查看。基于这个基础,这次我们希望增加 AI 聊天室功能。由于 Gemini 的免费 API 需要外网访问,这次的 UI 展示采用 Web 网页形式。

在保证跨平台兼容性的同时,Flutter 提供了良好的 UI 渲染能力,能够实现高效、实时的数据展示。核心挑战在于:

  1. 实现 HTTP 流式通信,实时接收 AI 的回复。
  2. 处理 Gemini API 返回的 复杂 JSON 数据结构
  3. 构建 高性能、沉浸式的聊天 UI,支持 Markdown 渲染和打字机效果。

与 Gemini API 集成

1. 使用 Dio 实现 HTTP 流

我们通过 Dio 来实现对 Gemini API 的访问,并支持流式响应。关键配置如下:

final response = await _dio.post<ResponseBody>(
  '/$model:streamGenerateContent',
  queryParameters: {'key': _apiKey},
  data: {
    'contents': contents,
  },
  options: Options(responseType: ResponseType.stream),
);
  • responseType: ResponseType.stream 允许我们 不等待完整响应体,而是实时接收数据块 (chunk)。
  • 这样做的好处是 极大降低延迟,在用户输入后可以立即看到 AI 的部分回复。

2. SSE 流式数据解析

Gemini API 返回的是 SSE(Server-Sent Events)流,一个完整的 JSON 事件可能被拆分在多个 chunk,或者一个 chunk 中包含多个事件。如果解析不当,JSON 可能解析失败。

我们使用一个 缓冲区字符串 来管理这些数据:

String buffer = '';
await for (final chunk in stream) {
  final text = utf8.decode(chunk);
  buffer += text; // 累加到缓冲区
  final lines = buffer.split('\n'); // 按行分割
  buffer = lines.last; // 将不完整的行保留到缓冲区
  for (int i = 0; i < lines.length - 1; i++) {
    final line = lines[i].trim();
    if (line.startsWith('data: ')) {
      final jsonStr = line.substring(6);

      try {
        final map = jsonDecode(jsonStr) as Map<String, dynamic>;
        final candidates = map['candidates'] as List<dynamic>?;

        final t = candidates?[0]['content']?['parts']?[0]['text'] as String?;

        if (t != null && t.isNotEmpty) {
          yield t; // 实时输出给 UI
        }
      } catch (e) {
        if (kDebugMode) {
          print('JSON 解析错误: $e');
        }
      }
    } else if (line == 'data: [DONE]') {
      return; // 流式结束
    }
  }
}

这里的重点是:

  • 使用 缓冲区 累积数据,保证完整的 JSON 能够被正确解析。
  • 使用 安全级联操作符 (?.) 提取嵌套 JSON 中的文本。
  • 通过 yield 将流式数据发送给 UI,实现实时显示。

3. Gemini API JSON 响应结构解析

Gemini 返回的文本片段路径复杂:

{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "AI 回复文本"
          }
        ],
        "role": "model"
      }
    }
  ]
}

提取方法如下:

final t = candidates?[0]?['content']?['parts']?[0]?['text'] as String?;

通过这种方式,可以 安全地访问深层嵌套的 JSON 字段,避免空指针或异常崩溃。


UI 实现

1. 高效列表展示

聊天界面使用 ListView.builder 构建,可以 动态加载消息,保证性能:

ListView.builder(
  controller: _scrollController,
  padding: const EdgeInsets.all(16),
  itemCount: _messages.length,
  itemBuilder: (context, index) {
    return _buildMessageBubble(_messages[index], theme);
  },
);

2. 单条消息展示

通过 _buildMessageBubble 函数区分 用户消息AI 回复

  • 用户消息:右对齐,浅色背景。
  • AI 回复:左对齐,深色背景,支持 Markdown 渲染。
Padding(
  padding: const EdgeInsets.symmetric(vertical: 8),
  child: Row(
    mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (!isUser) ...[
        CircleAvatar(
          backgroundColor: theme.colorScheme.primaryContainer,
          child: Icon(Icons.smart_toy, color: theme.colorScheme.onPrimaryContainer, size: 20),
        ),
        const SizedBox(width: 8),
      ],
      Flexible(
        child: Container(
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: isUser ? theme.colorScheme.primary.withOpacity(0.1) : theme.colorScheme.surfaceContainerHighest,
            borderRadius: BorderRadius.only(
              topLeft: const Radius.circular(16),
              topRight: const Radius.circular(16),
              bottomLeft: Radius.circular(isUser ? 16 : 4),
              bottomRight: Radius.circular(isUser ? 4 : 16),
            ),
            border: Border.all(
              color: isUser ? theme.colorScheme.primary.withOpacity(0.3) : theme.colorScheme.outlineVariant,
              width: 0.5,
            ),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (message.content.isEmpty && message.isStreaming)
                _buildTypingIndicator(theme)
              else
                GptMarkdown(message.content),
              if (message.isStreaming && message.content.isNotEmpty)
                Padding(
                  padding: const EdgeInsets.only(top: 8),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      SizedBox(
                        width: 12,
                        height: 12,
                        child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary),
                      ),
                      const SizedBox(width: 6),
                      Text('生成中...', style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.outline)),
                    ],
                  ),
                ),
            ],
          ),
        ),
      ),
      if (isUser) ...[
        const SizedBox(width: 8),
        CircleAvatar(
          backgroundColor: theme.colorScheme.primary,
          child: Icon(Icons.person, color: theme.colorScheme.onPrimary, size: 20),
        ),
      ],
    ],
  ),
);

3. 打字机效果

为了增强沉浸感,我们为 AI 回复添加 打字机动画

_typingTimer = Timer.periodic(const Duration(milliseconds: 40), (t) {
  if (!mounted) {
    t.cancel();
    return;
  }
  setState(() {
    final lastIndex = _messages.length - 1;
    final lastMessage = _messages[lastIndex];
    final nextIdx = (idx + step) > full.length ? full.length : (idx + step);
    _messages[lastIndex] = lastMessage.copyWith(
      content: lastMessage.content + full.substring(idx, nextIdx),
    );
    idx = nextIdx;
    if (idx >= full.length) {
      _messages[lastIndex] = _messages[lastIndex].copyWith(isStreaming: false);
      _isLoading = false;
      t.cancel();
    }
  });
  _scrollToBottom();
});
  • 利用 Timer.periodic 按时间间隔逐步展示文本。
  • 每次只追加一小段字符 (step = 24)。
  • 实现 AI 回复逐字生成的动态效果。

最终效果

界面支持:

  • 左右分辨用户与 AI 消息
  • Markdown 渲染,支持代码块、列表、粗体
  • 打字机动画,增加沉浸感
  • 滚动到底部自动显示最新消息

效果截图
Markdown 渲染示例


总结

通过本文的示例,我们实现了一个 高性能、实时流式的 AI 聊天室

  1. 使用 Dio 流式请求,实现底层网络到 UI 的实时数据传输。
  2. 处理 复杂 JSON 响应结构,安全提取文本。
  3. 构建 沉浸式聊天 UI,支持 Markdown 渲染和打字机动画。
  4. UI 可直接迁移至 OpenHarmony 或 Web 平台,只需替换 API 即可。

这个实现为 AI 聊天、知识问答以及智能助手类应用提供了 稳定、高效的技术方案,可作为 Flutter 与 AI 流式应用开发的参考模板。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐