WuKongIM Docs

Flutter 快速接入

精确安装 WuKongEasySDK Flutter 1.1.0,用可连续复制的 Dart 代码完成连接、在线收发与 Widget 清理。

编辑此页报告文档问题

使用一个 StatefulWidget 依次完成安装、初始化、监听、单聊发送和 dispose 清理。Alice 与 Bob 运行在两个独立设备、模拟器或浏览器上下文中。

当前 Product Gateway 支持这条连接路径

当前 Product Gateway 支持固定 v1.1.0 的 JSON-RPC CONNECT 与在线双向收发:客户端以 Base64 发送,服务端对有效 JSON 输出对象 RECV。设备线路值为 APP 0、WEB 1、PC 2。教程使用 pub.dev 1.1.0 与对应 Release;同一 revision 98ab8f3d9a1ad53f40c32caef0979845a37ae9a6 的官方 example 与从 pub.dev 解析的正式包均已在 iOS Simulator 完成双向消息和断开。其他 Flutter 目标、WSS 与生产 Token 校验仍须独立验收。

完成后你会得到什么

  • pubspec.yaml 中锁定为 1.1.0 的依赖;
  • 一个不会在 Widget 重建时重复注册监听器的页面;
  • Alice 的发送结果与 Bob 的实时消息事件;
  • 清晰的退出、去重、Token、WSS 与后续离线能力边界。

先运行官方 example(推荐)

git clone https://github.com/WuKongIM/WuKongEasySDK-Flutter.git
cd WuKongEasySDK-Flutter
git checkout 98ab8f3d9a1ad53f40c32caef0979845a37ae9a6
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter run -d <device-id>

这个 revision 就是正式 v1.1.0;源码 example 和 hosted 正式包的完整服务端准备、设备地址及 Alice/Bob 验收见运行官方示例

开始前

准备以下条件:

  • Flutter 3.0 或更高版本;
  • Dart 3.0 或更高版本;
  • 一个 /readyz 正常、目标设备可访问 WebSocket Gateway 的 WuKongIM 单节点集群或多节点集群;
  • 业务后端能为 Alice 和 Bob 分别返回 uid、短期 tokenwebsocketUrl

先阅读身份与 Token。如果应用同时构建移动端、桌面端和 Web,请把每个目标分别验收,不能用一个平台的结果推断全部平台。

步骤 1:安装精确版本

pubspec.yaml 中添加精确版本:

dependencies:
  flutter:
    sdk: flutter
  wukong_easy_sdk: 1.1.0

然后执行:

flutter pub get

提交更新后的 pubspec.lock,确保 CI 与开发机安装同一版本。

步骤 2:接收业务后端的连接材料

class IMBootstrap {
  const IMBootstrap({
    required this.uid,
    required this.token,
    required this.websocketUrl,
  });

  final String uid;
  final String token;
  final String websocketUrl;
}

业务后端先验证产品登录,再返回这三个字段。客户端不持有 Product HTTP 管理凭据,生产地址使用 wss://

步骤 3:初始化并保存监听器引用

以下页面只在 initState 触发一次初始化。每个回调都保存为字段,才能在 dispose 中传回同一个函数引用。

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:wukong_easy_sdk/wukong_easy_sdk.dart';

class ChatPage extends StatefulWidget {
  const ChatPage({required this.bootstrap, super.key});

  final IMBootstrap bootstrap;

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final easySDK = WuKongEasySDK.getInstance();
  final messagesById = <String, Message>{};

  late final WuKongEventListener<ConnectResult> connectListener;
  late final WuKongEventListener<DisconnectInfo> disconnectListener;
  late final WuKongEventListener<Message> messageListener;
  late final WuKongEventListener<WuKongError> errorListener;

  bool listenersRegistered = false;
  bool connected = false;

  @override
  void initState() {
    super.initState();
    _createListeners();
    _start();
  }

  void _createListeners() {
    connectListener = (result) {
      if (!mounted) return;
      setState(() => connected = true);
      debugPrint('connected');
    };
    disconnectListener = (info) {
      if (!mounted) return;
      setState(() => connected = false);
      debugPrint('disconnected');
    };
    messageListener = (message) {
      if (!mounted) return;
      setState(() => messagesById[message.messageId] = message);
    };
    errorListener = (error) {
      debugPrint('EasySDK operation failed');
    };
  }

  Future<void> _start() async {
    final config = WuKongConfig(
      serverUrl: widget.bootstrap.websocketUrl,
      uid: widget.bootstrap.uid,
      token: widget.bootstrap.token,
      debugLogging: false,
    );

    try {
      await easySDK.init(config);
      if (!mounted) {
        easySDK.dispose();
        return;
      }
      _registerListeners();
      await easySDK.connect().timeout(
        const Duration(seconds: 20),
        onTimeout: () {
          easySDK.disconnect();
          throw TimeoutException('EasySDK connect exceeded 20 seconds');
        },
      );
    } catch (_) {
      easySDK.disconnect();
      debugPrint('EasySDK connect failed');
    }
  }

  void _registerListeners() {
    if (listenersRegistered) return;
    easySDK.addEventListener(WuKongEvent.connect, connectListener);
    easySDK.addEventListener(WuKongEvent.disconnect, disconnectListener);
    easySDK.addEventListener(WuKongEvent.message, messageListener);
    easySDK.addEventListener(WuKongEvent.error, errorListener);
    listenersRegistered = true;
  }

  @override
  void dispose() {
    if (listenersRegistered) {
      easySDK.removeEventListener(WuKongEvent.connect, connectListener);
      easySDK.removeEventListener(WuKongEvent.disconnect, disconnectListener);
      easySDK.removeEventListener(WuKongEvent.message, messageListener);
      easySDK.removeEventListener(WuKongEvent.error, errorListener);
    }
    easySDK.disconnect();
    easySDK.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(connected ? 'Online' : 'Connecting')),
      body: ListView(
        children: messagesById.values
            .map((message) => ListTile(title: Text(_displayPayload(message.payload))))
            .toList(),
      ),
    );
  }

  String _displayPayload(dynamic payload) {
    if (payload is String) {
      try {
        return utf8.decode(base64Decode(payload));
      } catch (_) {
        return payload; // 保留未知或非 Base64 Payload,交给降级 UI。
      }
    }
    return jsonEncode(payload);
  }
}

debugLogging: false 与 SDK 默认值相同,这里显式写出以便审查生产配置。只有在受控诊断窗口中才设置为 true;若同时传入 logHandler,它只会收到 SDK 已脱敏的运行元数据,应用仍不得把完整事件、模型或 Payload 追加进去。

页面级 dispose() 适合最小示例。Future.timeout 把完整连接等待限制为 20 秒,并在超时或其他失败后断开;不要让页面永久停在 Connecting。真实应用如果希望切换页面时连接保持,应让 Provider、Riverpod、Bloc 或其他应用级状态容器拥有 SDK;页面只添加和移除自己的监听器,最终退出账号时再 disconnectdispose

步骤 4:发送第一条消息

_ChatPageState 中加入:

Future<void> sendText(String targetUid, String text) async {
  if (!connected) throw StateError('EasySDK is not connected');

  await easySDK.send(
    channelId: targetUid,
    channelType: WuKongChannelType.person,
    payload: {
      'type': 1,
      'version': 1,
      'content': text,
    },
  );

  debugPrint('SEND completed');
}

调用 sendText('bob', 'Hello from Flutter EasySDK') 后,Alice 记录 SendResult。Bob 必须在自己的 messageListener 中独立看到消息;发送结果和实时接收不是同一个事件。

步骤 5:用 Alice 和 Bob 验收

  1. 在两台设备、两个模拟器或两个独立浏览器上下文中分别启动 Alice 与 Bob;
  2. 两端都等到 WuKongEvent.connect 后再启用发送;
  3. Alice 向个人 Channel bob 发送消息,记录 ID、序号和 Reason Code;
  4. Bob 核对 fromUidchannelId,检查对象 Payload 的 typecontent,并按 messageId 去重;
  5. Bob 向 Alice 回发,验证反向链路;
  6. 销毁并重新打开页面,确认没有重复监听,再验证退出账号后的资源释放。

上述正式 v1.1.0 example 已在 iOS Simulator 连接同一版 WuKongIM,完成双向消息;25 个测试和静态分析也通过。从 pub.dev 解析且 package config 标记为 hosted 的同版本产物随后在托管 iOS Simulator 再次完成双向消息和断开。该闭环没有证明 Android、Web、桌面目标、应用后台恢复、离线消息同步或多端会话一致性,继续按消息收发上线检查补齐。

常见问题

  • 初始化或切换 UID 后状态异常:等待 easySDK.init(config) 完成后再连接;切换账号前先移除监听、断开并 dispose,同时清空产品本地状态。
  • Widget 重建后重复收消息:不要在 build 中注册监听器;保存回调引用并在 dispose 移除。
  • 系统日志仍出现 Token 或 Payload:先确认锁文件实际解析到 wukong_easy_sdk 1.1.0,并检查应用回调、debugPrint、自定义 logHandler 与采集器是否记录了完整事件或模型;SDK 诊断默认关闭,显式开启时也只应输出脱敏运行元数据。用 Release 产物复现并保留低敏证据后再提交问题。
  • message.payload 是一段字符串:当前服务端对有效 JSON 对象输出对象;若仍收到字符串,先确认服务端 revision,再尝试按 Base64 → UTF-8 JSON 解码,未知格式保留原值并降级展示。
  • 真机连不上本地地址localhost 指向真机自身,让业务后端返回设备可达的 WSS 地址。
  • 发送成功但 UI 没有对方消息:把发送结果、实时接收和离线恢复分开检查。

上线前检查

  • 为 Android、iOS、Web、桌面等每个实际目标分别保存构建与运行结果,不跨平台推断;
  • 使用 wss://,验证证书、代理 Upgrade、连接超时、前后台切换与断网恢复;
  • debugLogging 默认关闭;生产保持 false,若使用 logHandler,只能接收 SDK 已脱敏元数据,应用不得追加 Token、Payload 或完整模型;
  • 按 APP 0、WEB 1、PC 2 核对设备值,保存 pubspec.lock、Release 产物、设备、网络和服务端 revision;
  • 使用上线验收继续验证离线、推送、多设备、容量、升级与回滚。

下一步

回到 WuKongEasySDK 概览,或继续阅读消息收发会话与未读数上线检查

本页内容