初始化、登录与连接
本页介绍客户端的生命周期:创建客户端和全部选项、登录(凭证、密码、注册)、恢复登录、退出与修改密码、会话结束的原因、令牌续期、连接状态、前后台切换与网络变化、本地数据库与加密、设备标识、日志与诊断、应用的运行配置,以及在后台 isolate 中使用的轻量客户端。
典型的用法是:在 main() 中创建客户端;im.auth.currentUser 为 null 时显示登录页,用业务服务端签发的凭证登录;监听 im.auth.stateStream,会话结束时回到登录页;监听 im.connection.stateStream,在界面上显示“连接中”“网络不可用”等状态。
创建客户端
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final im = await DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
));
runApp(MaterialApp(home: Text(im.auth.currentUser == null ? '未登录' : '已登录')));
}- 先初始化 Flutter 绑定:
DRFlutterPlatform.instance要监听 App 的前后台状态,必须在WidgetsFlutterBinding.ensureInitialized()之后使用。 - 一个进程一个客户端:同一进程中同一个 AppKey 只有一个客户端。再次调用
DRClient.create返回已有的客户端,这次的选项被忽略(apiUrl或wsUrl不同时记一条warn日志)。同时发出的多次调用也只创建一次。请在main()中创建,把它传给需要的页面,不要在 Widget 中反复创建。同一个 App 要同时使用两个应用时,可以各建一个客户端,它们互不相干。 - 返回前读取登录状态:
create返回之前读取安全存储、打开本地存储,im.auth.state、im.auth.currentUser马上可用。这台设备上已经登录过、会话还没有结束的,SDK 自动恢复登录,见恢复登录。创建时不等待网络。 - 与进程同生命周期:客户端没有释放的方法,App 进程结束时随之结束。开发时的热重载不重新执行
main(),客户端保持不变;热重启会重建 Dart 的全部状态,main()重新创建客户端。 appKey、apiUrl格式不对,appVersion超长,或conversationWindow小于 1 时,以local_validation失败,details.field指出是哪个选项;开启了encryptDatabase而 App 没有链接 SQLCipher 时以unsupported失败,见加密本地数据库。
在 Dart 命令行程序或测试中使用时,platform 传入 package:deeprespond_im/io.dart 中的 IoPlatform(指定数据目录),用法与 Flutter 中相同。
发送队列
发出的消息先进入发送队列,保存在本地数据库中,由 SDK 负责发送和重试;App 被关闭后再次打开时继续发送。outbox 选项控制两个上限:
maxAge:超过这个时间仍没发出的消息不再发送,标为发送失败(错误码send_abandoned),默认 48 小时;persistAttachmentMaxBytes:图片、视频、文件等附件在发送前复制一份保存到 SDK 的目录中,App 被关闭后也能继续上传;超过这个大小的不复制,只记下原文件的路径,原文件在发出前被删除或移动时这条消息发送失败。默认 200 MB。
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
Future<DRClient> createClient() => DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
outbox: const OutboxOptions(maxAge: Duration(hours: 24), persistAttachmentMaxBytes: 100 * 1024 * 1024),
));退出前检查未完成的工作
im.hasPendingWork 表示是否还有没发出的消息或没完成的上传。用户退出登录前可以据此提醒:
if (im.hasPendingWork) {
showToast('还有消息正在发送,退出后将不再发送');
}没发出的消息在 App 再次打开后继续发送,见发送队列。
初始化选项
| 选项 | 类型 | 必填 | 说明 |
|---|---|---|---|
appKey | String | 是 | 应用的 AppKey,格式为 org_name#app_name,在控制台的应用详情中查看 |
apiUrl | Uri | 是 | IM 服务的地址,如 Uri.parse('https://im.example.com'),必须是 http 或 https |
platform | DRPlatform | 是 | 平台实现。Flutter App 中传 DRFlutterPlatform.instance(deeprespond_im_flutter),它给出平台标识(ios、android、macos、windows、linux)、前后台、网络变化、安全存储和设备信息 |
wsUrl | Uri? | 否 | 长连接的地址。默认把 apiUrl 的协议换成 wss(http 换成 ws)并加上 /client/v1/ws |
deviceName | String? | 否 | 设备名称,显示在登录设备列表和“你的账号已在 XX 上登录”的提示中。默认取系统提供的名称,见设备标识与设备名称。最长 64 个字符,超长截断 |
appVersion | String? | 否 | 你的 App 的版本号,随长连接上报,便于排查问题。默认取 App 的版本号(pubspec.yaml 中的 version)。按 UTF-8 编码最长 64 字节 |
ticketProvider | TicketProvider? | 否 | 重新获取登录凭证的函数 Future<String> Function(TicketContext),建议使用凭证登录的 App 都提供,见自动重新获取凭证 |
encryptDatabase | bool | 否 | 用 SQLCipher 加密本地数据库,默认 false,见加密本地数据库 |
autoMarkRead | bool | 否 | 用户正在看一个会话时,自动把新消息标记为已读。默认 false,见自动标记已读 |
conversationWindow | int | 否 | 会话列表在内存中最多保留的会话数,默认 1000,见会话列表的窗口 |
userCacheTtl | Duration | 否 | 其他用户资料的缓存时间,默认 10 分钟 |
outbox | OutboxOptions | 否 | 发送队列:maxAge(未发出的消息最多保留多久,默认 48 小时)、persistAttachmentMaxBytes(随发送队列复制保存的附件的上限,默认 200 MB),见发送队列 |
media | MediaOptions | 否 | 图片:imageMaxSide(发送前压缩的长边,默认 2048 像素)、jpegQuality(默认 85)、imageCacheBytes(DRImage 的磁盘缓存上限,默认 200 MB),见文件与图片 |
badgeProvider | int Function()? | 否 | App 切到后台时上报给服务端的角标数,默认为未读总数(不计免打扰的会话),见角标 |
push | DRPushOptions? | 否 | 离线推送的选项(需要依赖 deeprespond_im_push),不给时不登记推送,见离线推送 |
call | DRCallOptions? | 否 | 系统来电界面的选项(需要依赖 deeprespond_im_call),不给时不接入系统来电界面,见音视频通话 |
log | LogOptions | 否 | 日志级别和输出函数,见日志 |
自动标记已读
autoMarkRead 为 true 时,用户“正在看”一个会话期间收到的新消息自动标记为已读。“正在看”指:这个会话有打开着的消息列表,列表通过 setViewing() 报告停在最新处(atBottom)且在界面上显示(visible),并且 App 在前台(没有报告过的按 true 处理)。默认 false,这时由你在合适的时机调用 list.markRead() 或 im.conversations.markRead(),见会话。开启后,被其他页面盖住但没有释放的消息列表要报告 visible: false,否则用户没看到的消息也会被标为已读。
会话列表的窗口
conversationWindow 是会话列表在内存中最多保留的会话数,默认 1000。im.conversations.list() 第一次从本地读取最近的这么多个会话,之后调用 loadMore() 每次再读 100 个,本地读完后再向服务端取更早的会话。会话很多、内存紧张时可以调小。未读总数 getUnreadTotal() 总是覆盖全部会话,不受这个窗口限制。
角标
App 切到后台时,SDK 把 App 图标上应显示的角标数上报给服务端,之后离线推送中的角标从这个数开始累加。默认上报未读总数(不计免打扰的会话,与 im.conversations.getUnreadTotal(excludeMuted: true) 相同)。角标的算法与默认不同(如只计单聊,或加上你自己的业务角标)时,用 badgeProvider 返回要上报的数:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
int businessBadge = 0; // 你自己的业务角标
late final DRClient im;
Future<void> createClient() async {
im = await DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
badgeProvider: () => im.conversations.getUnreadTotal(excludeMuted: true) + businessBadge,
));
}badgeProvider 在切到后台时同步调用,请只做简单的计算。运行配置的 pushBadgeEnabled 为 false 时不上报角标,也不调用它。没有登记推送的设备不上报。推送的登记和 App 图标上角标的显示见离线推送。
设备标识与设备名称
SDK 为每台设备生成一个设备标识(flutter_ 加 32 位十六进制随机数),之后不变,你不需要处理。服务端按设备标识区分登录设备:同一台设备再次登录会替换这台设备原来的登录,不会多占设备数。
| 平台 | 保存位置 | 重新安装 App 后 |
|---|---|---|
| iOS、macOS | 钥匙串(首次解锁后可读、仅本设备) | 通常仍是同一个设备标识,按同一台设备处理;钥匙串中的登录状态通常也还在,App 打开后直接恢复登录 |
| Android | App 数据目录中不参与备份的文件 | 新的设备标识。旧的登录在过期或被踢之前仍占用设备名额,应用设置为拒绝新设备登录时,可能得到 device_limit_exceeded |
| Windows、Linux | 应用支持目录中的文件 | 取决于卸载时是否删除了这个目录 |
设备标识不会通过备份或换机迁移带到另一台设备,两台设备不会因为共用一个设备标识而互相替换登录。
设备名称默认取系统提供的名称:
| 平台 | 默认的设备名称 |
|---|---|
| iOS | 系统的设备名称。iOS 16 起只能取到通用名称,如“iPhone”“iPad” |
| Android | 厂商加型号代码,如“Xiaomi 23127PN0CC” |
| macOS、Windows | 计算机名 |
| Linux | 发行版名称,如“Ubuntu 24.04 LTS” |
想显示更友好的名称(如手机的营销名“小米 14”),用 deviceName 指定。
登录
客户端支持两种登录方式,与服务端文档中的客户端登录相同:
| 方式 | 方法 | 适用场景 |
|---|---|---|
| 登录凭证(推荐) | im.auth.loginWithTicket() | 已有账号体系的应用:用户登录你的业务系统后,业务服务端签发登录凭证,App 用凭证登录 IM |
| 密码 | im.auth.loginWithPassword() | 由 IM 服务保存用户密码的应用 |
登录时 SDK 自动带上 AppKey、设备标识、设备名称、平台和 SDK 版本,你只需要提供凭证或用户名和密码。
用登录凭证登录
final result = await im.auth.loginWithTicket(ticket: ticket);
print('${result.user.nickname} ${result.config.maxMessageBodyBytes} ${result.sessionId}');凭证 5 分钟内有效、只能使用一次:提交登录后即作废,即使登录失败也要重新获取。可以同时传 username,由服务端校验凭证属于这个用户。
自动重新获取凭证
创建客户端时提供 ticketProvider,SDK 在两种情况下调用它重新获取凭证:
- 用凭证登录返回
invalid_credentials(凭证无效、已使用或已过期)时,重新获取一次再登录,context.reason为invalid_credentials;仍然失败的才把错误交给你; - 登录状态过期(用户很久没有打开 App,超过了续期的有效期)时,静默重新登录一次,
context.reason为session_expired,context.username为原来的用户。成功时用户不会感觉到,失败时会话结束。
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
/// 向你的业务服务端获取当前用户的 IM 登录凭证;业务系统未登录时抛出异常。
Future<String> fetchTicket({String? expectedUser}) async => fetchTicketFromYourServer();
Future<DRClient> createClient() => DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
ticketProvider: (context) {
// context.reason:invalid_credentials 或 session_expired;context.username:原来的用户
return fetchTicket(expectedUser: context.username);
},
));ticketProvider 应只为当前登录你的业务系统的用户签发凭证。它抛出的异常:在用凭证登录时原样从 loginWithTicket 抛出;在静默重新登录时按重新登录失败处理,会话结束。被踢下线、被封禁、修改了密码等原因结束的会话不会自动重新登录。
用密码登录
await im.auth.loginWithPassword(username: 'zhangsan', password: 'Passw0rd!');用户名不区分大小写。用户需要先设置密码,见创建用户。
注册
应用的注册模式为 open 时,用户可以在 App 中自行注册,见客户端自注册。注册不会自动登录:
await im.auth.register(username: 'xiaoming', password: 'secret123', nickname: '小明');
await im.auth.loginWithPassword(username: 'xiaoming', password: 'secret123');新建的应用默认不允许客户端注册(返回 permission_denied),只建议在测试应用中开启。
切换用户
已有另一个用户登录时,直接登录新用户即可:SDK 先让原来的用户退出(本地数据保留),再登录新用户。原来用户打开着的列表被清空并释放,登录返回后请为新用户重新创建列表。切换期间登录状态一直是 SignedIn,不发出 AuthStateChanged,所以请以登录方法的返回为准刷新界面,或比较 im.auth.currentUser 的用户名。
登录失败
登录失败时抛出 DRException,登录状态不变:
code | 原因 | 建议 |
|---|---|---|
invalid_credentials | 用户名或密码错误;凭证无效、已使用或已过期 | 密码登录提示“用户名或密码错误”;凭证登录时已提供 ticketProvider 的,SDK 已经重新获取过一次 |
user_disabled | 用户已被封禁 | 提示“账号已被封禁” |
too_many_attempts | 密码错误次数过多,暂时不能用密码登录 | 按 e.retryAfter(秒)提示用户稍后再试,不要自动重试 |
device_limit_exceeded | 登录设备数已满,且应用设置为拒绝新设备登录 | 提示用户在其他设备上退出;凭证已作废,重试要重新获取 |
tenant_unavailable、app_unavailable | 服务已暂停 | 提示“服务暂时不可用” |
not_found | AppKey 对应的应用不存在 | 检查 appKey 的配置 |
rate_limited | 登录过于频繁 | SDK 已在等待时间不超过 60 秒时自动重试一次;仍失败的按 e.retryAfter 稍后再试 |
network_error、timeout、internal | 网络或服务端故障 | 稍后重试;凭证登录要重新获取凭证 |
local_validation | 凭证、用户名或密码为空 | 请求没有发出,检查参数 |
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im/render.dart';
try {
await im.auth.loginWithPassword(username: 'zhangsan', password: 'wrong');
} on DRException catch (e) {
if (e.code == 'invalid_credentials') {
showToast('用户名或密码错误');
} else {
showToast(describeError(e));
}
}恢复登录
App 启动时,这台设备上已经登录过、会话还没有结束的,DRClient.create 返回后立即处于已登录状态(im.auth.state 为 SignedIn):
- 本地保存的会话列表、消息等数据立即可以读取,可以马上显示界面,不必等待网络;
- SDK 在后台连接长连接、同步离线期间的变化,完成后连接状态变为
ConnectionReady,并发出SyncCompleted事件; - 令牌已过期的,SDK 先续期再连接。
所以 App 启动时按 im.auth.currentUser 是否为 null 决定显示登录页还是主界面:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:flutter/material.dart';
Widget home(DRClient im) => im.auth.currentUser != null ? const Text('主界面') : const Text('登录页');退出
await im.auth.logout();
// 多人共用的设备上:退出并删除这个用户在本地的聊天记录
await im.auth.logout(clearLocalData: true);- 退出在本地立即生效:清除令牌、断开长连接,打开着的列表被清空并释放,
im.auth.state变为SignedOut。依赖了推送包时,通知栏中本 App 的通知一并清除。 - SDK 同时通知服务端结束这次登录,释放设备名额,这台设备的推送令牌随之失效(不必另外注销推送)。网络不通时 SDK 记下来,之后网络恢复或下次启动时补发,
logout()不会因此失败。 - 默认保留本地数据:同一用户再次登录时可以立即显示之前的会话和消息。
要删除不是当前登录用户的本地数据(如“清除本机上的聊天记录”),使用 clearLocalData:
await im.auth.clearLocalData(username: 'lisi'); // 删除 lisi 在本地的数据
await im.auth.clearLocalData(all: true); // 删除本应用的全部本地数据,只能在未登录时调用不能用它删除当前登录用户的数据,请改用 logout(clearLocalData: true)。
修改密码
await im.auth.changePassword(currentPassword: 'Passw0rd!', newPassword: 'N3wPassw0rd');成功后这台设备保持登录(SDK 换用新的令牌,并重新连接),这个用户的其他设备全部下线,下线原因为 password_changed。
- 当前密码错误抛出
invalid_credentials,不会结束会话,但计入密码错误次数;错误次数过多抛出too_many_attempts; - 没有设置过密码的用户(
im.users.me()?.hasPassword为false,如通过凭证登录自动创建的用户)调用抛出permission_denied; - 修改密码不会自动重试:网络错误时,请让用户确认后再操作。
会话结束
以下情况下这次登录被服务端结束,SDK 清除本地令牌、断开连接,im.auth.state 变为 SessionEnded(带 code、reason、details),并发出 AuthStateChanged。打开着的列表被清空并释放,你应回到登录页:
im.auth.stateStream.listen((state) {
if (state is! SessionEnded) return;
if (state.reason == 'device_limit' || state.reason == 'replaced') {
final device = state.details?['device_name'];
showToast(device is String && device.isNotEmpty ? '你的账号已在 $device 上登录' : '你的账号已在其他设备上登录');
} else if (state.reason == 'user_disabled') {
showToast('账号已被封禁');
} else {
showToast('登录已失效,请重新登录');
}
// 回到登录页
});code | reason | 原因 | 本地数据 |
|---|---|---|---|
session_revoked | device_limit | 登录设备数超出上限,被新设备挤下线。details['device_name']、details['platform'] 为新设备(可能为空) | 保留 |
session_revoked | replaced | 同一个设备标识在别处重新登录。details 同上 | 保留 |
session_revoked | kicked | 被业务服务端、控制台踢下线,或被用户本人在其他设备上踢下线 | 保留 |
session_revoked | password_changed | 密码在其他设备上被修改,或被服务端重置 | 保留 |
session_revoked | user_disabled | 用户被封禁 | 保留 |
session_revoked | relogin_required | 控制台要求应用内全部用户重新登录 | 保留 |
session_revoked | token_reuse | 续期令牌被重复使用,可能已经泄露 | 保留 |
session_revoked | expired | 登录过期(超过有效期没有打开 App)。提供了 ticketProvider 的,SDK 先静默重新登录一次,失败才结束 | 保留 |
session_revoked | user_deleted | 用户被删除 | 删除这个用户的本地数据 |
unauthenticated | app_deleted、tenant_closed | 应用已删除,或租户已注销 | 删除本应用的全部本地数据 |
unauthenticated | 无 | 续期令牌已失效。提供了 ticketProvider 的,SDK 先静默重新登录一次 | 保留 |
下线原因的完整说明见服务端文档的下线原因。本人主动调用 logout() 时状态变为 SignedOut,不是 SessionEnded,不需要提示。
服务暂停
租户或应用被暂停服务(如欠费)时,im.auth.state 变为 Suspended:code 为 tenant_unavailable 或 app_unavailable,retryAt 为 SDK 下一次尝试恢复的时刻(毫秒时间戳)。这期间登录保持(im.auth.currentUser 仍有值),本地数据照常可读,长连接和同步暂停,发出的消息在发送队列中等待。SDK 每隔几分钟检查一次,服务恢复后自动回到 SignedIn,继续连接和发送,不需要用户重新登录。reason 为 arrears 时可以提示“服务已暂停”。
应用处于只读状态时,写操作抛出 app_unavailable,但长连接和查询照常,im.auth.state 不变。
令牌续期
登录后得到的令牌有有效期(由运行策略决定,默认 1 天),SDK 在过期之前自动续期,你不需要处理。
- 令牌保存在系统的安全存储中:iOS、macOS 的钥匙串(首次解锁后可读、仅本设备,锁屏时被来电推送唤醒也能读到),Android 用 Keystore 加密保存,Windows 用当前用户的 DPAPI 加密,Linux 用 libsecret。令牌不写入普通文件和日志。
- App 切到后台后不主动续期;回到前台、网络恢复时立即检查是否该续期。令牌已过期的,下次连接或请求前先续期。
- 续期因网络原因失败时,SDK 在 5 分钟内多次重试;仍失败的等下一次需要令牌时再试,已有的连接不受影响。
- 用户在续期令牌的有效期(默认 7 天)内打开过 App,就一直保持登录;超过的,登录过期,见上文的
expired。 - 后台 isolate 中的
DRBackgroundClient也会在需要时续期,与主 isolate 不会同时续期,见后台 isolate 中的客户端。 - 已知的限制:续期请求已被服务端处理、而 App 没有收到响应,随后 App 又超过 10 分钟没有运行的,下次启动时会话会以
token_reuse结束,用户要重新登录。
SDK 的请求只把令牌发给 IM 服务,下载地址、对象存储和媒体服务的请求不带令牌。
连接状态
im.connection.state 是长连接的当前状态(DRConnectionState),变化时 im.connection.stateStream 发出新状态,同时发出 ConnectionStateChanged 事件:
| 类型 | kind | 含义 | 界面建议 |
|---|---|---|---|
ConnectionIdle | idle | 未登录、还没开始连接,或 App 在后台不连接 | |
ConnectionConnecting | connecting | 正在连接 | “连接中…” |
ConnectionAuthenticating | authenticating | 已连上,正在认证 | “连接中…” |
ConnectionSyncing | syncing | 已认证,正在同步离线期间的变化,pending 为还没完成的模块 | “收取中…” |
ConnectionReady | ready | 已就绪,实时收发消息 | 不显示 |
ConnectionWaiting | waiting | 连接断开,等待重连。retryAt 为下一次重连的时刻(毫秒时间戳),lastErrorCode、lastErrorReason 为最近一次断开的原因 | “连接已断开,正在重连” |
ConnectionOffline | offline | 系统报告网络不可用 | “网络不可用” |
ConnectionStopped | stopped | 已停止,不会自动重连,code 说明原因,见下表 | 按原因提示 |
连接断开后 SDK 按 1 秒起逐次翻倍、最长 60 秒的间隔自动重连;网络恢复、App 回到前台时立即重连。断开期间本地数据照常可读,发送的消息进入发送队列,连上后自动发出。
ConnectionStopped 的原因:
code | 原因 | 处理 |
|---|---|---|
upgrade_required | SDK 版本低于服务端要求的最低版本,details['min_version'] 为最低版本。同时停止发送和同步,本地数据只读,写操作抛出 invalid_state(details['reason'] 为 upgrade_required);另发出 SdkUpgradeRequired 事件 | 提示用户升级 App |
replaced_elsewhere | 同一次登录在别处建立了连接(通常是令牌被复制到了别处)。SDK 被替换后先在 5 到 10 秒后重连一次,重连成功后 60 秒内再次被替换才停止。details 中有新连接的信息 | 由用户决定是否退出后重新登录 |
invalid_auth | 认证的内容不合法,details['field'] 指出字段,通常是 appVersion 等配置有误 | 检查配置 |
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:flutter/material.dart';
class ConnectionBanner extends StatelessWidget {
const ConnectionBanner({super.key, required this.im});
final DRClient im;
@override
Widget build(BuildContext context) => StreamBuilder<DRConnectionState>(
stream: im.connection.stateStream,
initialData: im.connection.state,
builder: (context, s) {
final text = switch (s.data) {
ConnectionReady() || ConnectionIdle() || null => null,
ConnectionOffline() => '网络不可用',
ConnectionWaiting(:final retryAt) => '连接已断开,${((retryAt - DateTime.now().millisecondsSinceEpoch) / 1000).ceil()} 秒后重连',
ConnectionStopped(code: 'upgrade_required') => '请升级到最新版本',
ConnectionStopped() => '连接已停止',
_ => '连接中…',
};
return text == null ? const SizedBox.shrink() : MaterialBanner(content: Text(text), actions: const [SizedBox.shrink()]);
},
);
}需要在连接就绪后才做的事,可以用 waitUntilReady 等待:
await im.connection.waitUntilReady(timeout: const Duration(seconds: 10)); // 超时抛出 timeout一般不需要等待:消息、会话等操作在未连接时也可以调用,SDK 会排队或改用 HTTP 请求。
前后台与网络变化
DRFlutterPlatform 自动跟踪 App 的前后台状态和网络变化,你不需要调用任何方法。在 Android 和 iOS 上:
- 切到后台(
AppLifecycleState.hidden或paused):SDK 在长连接上告诉服务端 App 已在后台,并上报角标数(见badgeProvider)。服务端从此把这台设备当作不在线,之后的新消息以离线推送送达。5 秒内没有收到服务端的确认时 SDK 主动断开连接,保证不会漏掉推送。iOS 上 SDK 自动向系统申请这段时间的后台执行时间。 - 在后台:连接保持,心跳间隔加长(默认 120 秒)。系统挂起 App 或断开连接后 SDK 不重连,等回到前台;连接状态可能停在
ConnectionIdle或ConnectionWaiting。例外是来电振铃和通话中:SDK 在后台也保持连接,以便及时收到对方取消、挂断。 - 回到前台(
resumed):SDK 告诉服务端 App 已在前台;连接已断开的立即重连,连接还在的先确认连接可用;然后检查令牌是否该续期,并同步后台期间的变化。正在打开着的消息列表会补上新消息。 - 网络变化:网络从无到有、或在 Wi-Fi 与移动网络之间切换时,SDK 立即建立新连接,不等旧连接超时;新连接替换旧连接,其他人看到的在线状态不会先离线再上线。网络不可用期间不重连,连接状态为
ConnectionOffline。
AppLifecycleState.inactive(如下拉通知中心、来电时)不算切到后台。桌面端(macOS、Windows、Linux)一直按前台处理,最小化或失去焦点不影响连接。
切到后台、完成上报时发出 AppLifecycleReported 事件(background、badge),可用于调试角标。
运行配置
im.config 是应用的运行配置(ClientConfig),由控制台的运行策略决定,登录时取得并保存在本地,之后在同步中自动更新,变化时发出 ConfigChanged。未登录时为 null。
用它决定界面上显示哪些功能,例如没有开启音视频的应用不显示通话按钮:
final config = im.config;
final showCallButton = config?.rtcEnabled ?? false;
final showReactions = config?.messageReactionEnabled ?? false;
final maxBytes = config?.maxMessageBodyBytes ?? 5120;
im.on<ConfigChanged>().listen((e) {
print('通话按钮:${e.config?.rtcEnabled ?? false}');
});常用的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
maxOnlineDevicesPerUser | int? | 每个用户同时登录的设备数上限 |
maxMessageBodyBytes | int? | 一条消息的大小上限,单位为字节 |
recallWindowSeconds | int? | 撤回自己消息的时限,单位为秒,0 为不能撤回 |
singleReadAckEnabled、groupReadAckEnabled | bool | 是否开启单聊、群聊的已读回执 |
messageReactionEnabled | bool | 是否允许表情回应 |
maxPinnedMessagesPerConversation | int? | 每个会话最多置顶的消息数 |
messageRetentionDays | int? | 历史消息的保留天数 |
friendCheckEnabled | bool | 是否只能给好友发单聊消息 |
userBlacklistEnabled | bool | 是否允许拉黑。为 false 时不提供拉黑的入口 |
clientGroupCreateEnabled | bool | 是否允许在客户端建群 |
maxGroupMembers | int? | 群人数上限 |
maxUploadBytes | int? | 消息附件和群文件的大小上限,单位为字节 |
groupFileEnabled | bool | 是否允许在客户端上传群文件 |
presenceEnabled | bool | 是否允许查询和订阅在线状态 |
clientChatroomCreateEnabled | bool | 是否允许在客户端创建聊天室 |
rtcEnabled | bool | 是否开启音视频通话 |
rtcChannelsEnabled | bool | 频道配置开关,仍需应用灰度及实际准入 |
rtcChannelsProtocolVersion | int? | 频道协议版本,当前为 1 |
pushBadgeEnabled | bool | 是否在推送和切到后台时上报角标 |
reportEnabled | bool | 是否允许举报 |
各字段的含义和取值范围见运行策略。以后新增的字段可以从 im.config?.raw 中按服务端的字段名读取。
本地数据
存放位置与备份
本地数据库和发送队列的附件保存在 App 的沙盒中,其他 App 读不到;每个用户一个数据库。
| 平台 | 数据目录 | 备份 |
|---|---|---|
| Android | 不参与备份的目录(noBackupFilesDir) | 不参与云备份和换机迁移 |
| iOS | Application Support,设置了文件保护(设备首次解锁后可读写) | SDK 已排除 iCloud 备份 |
| macOS | Application Support | SDK 已排除 iCloud 备份 |
| Windows、Linux | 应用支持目录 |
聊天记录可以从服务端重新同步,没有必要备份。Android 上安全存储自己的文件默认参与自动备份,恢复到新手机后无法解密,用户要重新登录;请按快速集成在备份规则中排除它。图片的磁盘缓存在系统的缓存目录中,见文件与图片。
App 降级(安装了更旧的版本)后,本地数据库的结构比 SDK 认识的新时,SDK 删除这个数据库,从服务端重新同步,并发出 StorageReset(reason 为 schema_downgrade)。数据库文件损坏打不开时同样删除重建(reason 为 corrupted)。磁盘空间不足、写入失败时发出 StorageFull,可以提示用户清理手机空间。
加密本地数据库
本地数据库默认不加密,受系统的沙盒和文件保护。对安全要求高的 App 可以用 SQLCipher 加密,分两步:
在 App 自己的
pubspec.yaml中让sqlite3使用 SQLCipher 构建(依赖包不能替 App 设置):yamlhooks: user_defines: sqlite3: source: sqlcipher创建客户端时设置
encryptDatabase: true:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
Future<DRClient> createClient() => DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
encryptDatabase: true,
));- SDK 创建客户端时检查链接的是否为 SQLCipher,不是的以
unsupported(details['reason']为sqlcipher_required)失败,不会静默地不加密。 - 每个数据库的密钥是 SDK 生成的随机密钥,保存在系统的安全存储中。密钥丢失时(如 Android 上安全存储的数据被系统清除)数据库无法打开,SDK 删除它,从服务端重新同步。
- 开启或关闭加密后,原有的数据库打不开,SDK 同样删除重建、重新同步,还没发出的消息随之丢失。请在 App 的第一个版本就确定是否加密,不要在版本之间切换。
- 代价:读写通常慢 5% 到 15%;Android 安装包每个 ABI 增加约 3 到 4 MB。
诊断
im.connection.diagnostics() 返回排查问题所需的信息,用户反馈问题时可以附上:
final d = im.connection.diagnostics();
print('${d['sdkVersion']} ${d['sessionId']} ${d['connectionId']} ${d['connectionState']}');
print('${d['recentDisconnects']} ${d['recentFailures']}'); // 最近 20 次断开、最近 20 个失败的请求诊断信息不含令牌和消息内容。其中的 sessionId 与服务端查询登录设备中的 session_id 相同,recentFailures 中的 request_id 可以提供给技术支持查找请求(SDK 的每个请求都带 X-Request-ID,以 fl- 开头)。字段见诊断信息。
日志
SDK 默认只输出 warn 和 error 级别的日志,用 print 输出(在 flutter run 的控制台、Android 的 logcat、Xcode 的控制台中可以看到)。可以调整级别,或交给你自己的日志系统:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
final logs = <LogEntry>[];
Future<DRClient> createClient() => DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
log: LogOptions(
level: LogLevel.info,
sink: (entry) {
logs.add(entry);
if (logs.length > 500) logs.removeAt(0);
},
),
));日志分为 error、warn、info、debug 四级。日志中不包含令牌、密码、登录凭证、消息内容和文件地址的密钥部分,只记录请求名、错误码、request_id 和连接 ID,可以放心上报。要写入文件的,在 sink 中由你写入。sink 抛出的异常被忽略,不影响 SDK。LogEntry.toString() 给出一行可读的文字。
后台 isolate 中的客户端
App 不在前台时,Flutter 可能在单独的 isolate 中运行你的代码:FCM 的后台处理函数(App 在后台、锁屏或已被关闭时)、App 自己的后台任务。这些 isolate 中不能使用 DRClient,请使用轻量的 DRBackgroundClient:它不建立长连接、不读写本地数据库,只提供处理推送所需的少数操作——解析推送数据、查询通话、接听或拒绝来电、上报角标。
- App 的主 isolate 还在运行时,
DRBackgroundClient的操作转给主 isolate 的客户端执行(10 秒没有回复的自己执行);主 isolate 不在运行时,它自己用客户端接口执行,令牌过期时自己续期。 - 服务端地址取主 isolate 最近一次创建客户端时保存的地址,所以 App 至少要正常启动过一次;没有保存过时
open抛出invalid_state(details['reason']为no_launch_info),也可以用apiUrl直接给出。 - 使用推送包的,FCM 的后台处理函数调用
drFirebaseBackgroundHandler即可,来电由 SDK 处理,不需要你使用DRBackgroundClient,见离线推送和音视频通话。
在 App 自己的后台任务中上报角标的例子:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/widgets.dart';
@pragma('vm:entry-point')
Future<void> onBackgroundTask(int unread) async {
WidgetsFlutterBinding.ensureInitialized();
final client = await DRBackgroundClient.open(appKey: '1575529652#demo', platform: DRFlutterPlatform.instance);
try {
await client.reportBadge(unread);
} finally {
await client.close();
}
}接口参考
DRClient.create()
创建客户端。同一进程中同一个 AppKey 再次调用返回已有的客户端。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
options | DRClientOptions | 是 | 初始化选项,见初始化选项 |
返回值:Future<DRClient>,完成时本地的登录状态已经读取。
可能的错误:local_validation(选项格式不对,details['field'] 指出选项)、unsupported(开启了 encryptDatabase 而没有链接 SQLCipher,details['reason'] 为 sqlcipher_required)。
im.auth.loginWithTicket()
用业务服务端签发的登录凭证登录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
ticket | String | 是 | 命名参数。登录凭证,以 ult_ 开头 |
username | String? | 否 | 命名参数。凭证所属的用户名,给出时由服务端校验 |
返回值:Future<LoginResult>,完成时本地存储已经打开,可以立即创建列表。
可能的错误:见登录失败。ticketProvider 抛出的异常原样抛出。
im.auth.loginWithPassword()
用用户名和密码登录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | String | 是 | 命名参数。用户名,不区分大小写 |
password | String | 是 | 命名参数。密码 |
返回值:Future<LoginResult>。
可能的错误:见登录失败。
im.auth.register()
在客户端注册新用户。只在应用的注册模式为 open 时可用,注册后不会自动登录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | String | 是 | 命名参数。用户名,规则见用户名与资料 |
password | String | 是 | 命名参数。密码,1 到 64 个字符 |
nickname | String? | 否 | 命名参数。昵称,最长 64 个字符 |
返回值:Future<SelfProfile>,新用户的资料。
可能的错误:permission_denied(应用不允许客户端注册)、already_exists(用户名已被占用)、invalid_argument(参数不符合规则)、content_rejected(昵称未通过内容安全检查)、limit_exceeded(应用的注册用户数已满)、rate_limited。
im.auth.logout()
退出登录。本地立即生效。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
clearLocalData | bool | 否 | 命名参数。是否同时删除这个用户的本地数据,默认 false |
返回值:Future<void>。通知服务端失败时不抛出,SDK 稍后补发。未登录时直接完成。
im.auth.changePassword()
修改本人的密码。成功后其他设备下线,本设备保持登录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
currentPassword | String | 是 | 命名参数。当前密码 |
newPassword | String | 是 | 命名参数。新密码,1 到 64 个字符 |
返回值:Future<void>。
可能的错误:invalid_credentials(当前密码错误)、too_many_attempts(错误次数过多)、permission_denied(没有设置过密码)、invalid_argument(新密码不符合规则)、not_signed_in。
im.auth.clearLocalData()
删除指定用户在本地的数据(会话、消息、联系人等),不影响服务端的数据和登录状态。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | String? | 否 | 命名参数。要删除的用户,不能是当前登录的用户 |
all | bool | 否 | 命名参数。删除本应用的全部本地数据,只能在未登录时使用,默认 false |
两者给出一个。
返回值:Future<void>。
可能的错误:invalid_state(details['reason'] 为 signed_in:指定了当前登录的用户,或已登录时使用 all)、local_validation(两者都没有给出)。
im.auth.state
只读属性,当前的登录状态 AuthState,同步读取。
im.auth.stateStream
Stream<AuthState>,登录状态变化时发出新状态(与 AuthStateChanged 事件相同)。订阅时不发出当前状态,请先读取 im.auth.state。
im.auth.currentUser
只读属性,当前登录的用户 CurrentUser?(username、sessionId),未登录或会话已结束时为 null。服务暂停期间仍有值。username 为小写。
im.connection.state
只读属性,当前的连接状态 DRConnectionState。
im.connection.stateStream
Stream<DRConnectionState>,连接状态变化时发出新状态(与 ConnectionStateChanged 事件相同)。订阅时不发出当前状态,请先读取 im.connection.state。
im.connection.serverTimeOffsetMs
只读属性,int,服务端时间减本地时间的毫秒数,连接后更新。需要按服务端时间显示倒计时(如禁言到期)时,用 DateTime.now().millisecondsSinceEpoch + im.connection.serverTimeOffsetMs 作为当前时间。
im.connection.reconnectNow()
立即重连,不等退避的间隔结束(服务端要求的等待时间除外)。已连接、连接已停止(ConnectionStopped)或 App 在后台时不起作用。
返回值:无。
im.connection.waitUntilReady()
等到连接状态为 ConnectionReady。已经是时立即完成。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
timeout | Duration? | 否 | 命名参数。最长等待的时间,默认一直等待 |
cancel | DRCancelToken? | 否 | 命名参数。用于取消等待 |
返回值:Future<void>。
可能的错误:timeout(超时)、aborted(被取消)。
im.connection.diagnostics()
返回诊断信息。
返回值:Map<String, Object?>,见诊断信息。
im.config
只读属性,ClientConfig?,应用的运行配置,未登录时为 null。变化时发出 ConfigChanged。
im.events
Stream<DREvent>,全部事件,见事件与错误处理。
im.on()
im.on<T>() 返回只含类型 T 的事件的 Stream<T>,如 im.on<MessageReceived>()。只要一次时用 im.on<T>().first。
im.hasPendingWork
只读属性,bool,是否还有没发出的消息或没完成的上传。
DRBackgroundClient.open()
在后台 isolate 中打开轻量的后台客户端。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
appKey | String | 是 | 命名参数。应用的 AppKey |
platform | DRPlatform | 是 | 命名参数。Flutter 中传 DRFlutterPlatform.instance |
apiUrl | Uri? | 否 | 命名参数。IM 服务的地址,默认用主 isolate 创建客户端时保存的 |
log | LogOptions | 否 | 命名参数。日志选项 |
返回值:Future<DRBackgroundClient>。
可能的错误:invalid_state(details['reason'] 为 no_launch_info:没有给出 apiUrl,且这台设备上从未创建过客户端)。
backgroundClient.parse()
解析推送的数据,返回 PushPayload?(不是 IM 服务的推送时为 null)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
data | Map<String, Object?> | 是 | 推送的数据(如 FCM 消息的 data) |
channel | PushChannel? | 否 | 推送来自哪个通道 |
PushPayload 的各种类型见离线推送。
backgroundClient.getCall()
查询通话的简要信息。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
callId | String | 是 | 通话 ID |
返回值:Future<BackgroundCall?>,通话不存在或本人不在其中时为 null。
backgroundClient.answerCall()
接听来电(只接听,不打开麦克风和摄像头;App 回到前台后在通话界面中加入媒体,见音视频通话)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
callId | String | 是 | 通话 ID |
返回值:Future<void>。
backgroundClient.rejectCall()
拒绝来电。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
callId | String | 是 | 通话 ID |
reason | String | 否 | 命名参数。declined(拒绝,默认)或 busy(忙线) |
返回值:Future<void>。
backgroundClient.reportBadge()
向服务端上报 App 图标上的角标数(之后推送中的角标从这个数开始累加)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
badge | int | 是 | 角标数 |
返回值:Future<void>。
backgroundClient.close()
保存后台客户端的状态(如续期得到的新令牌)。用完后调用。
返回值:Future<void>。
数据结构
DRClientOptions
见初始化选项。
LoginResult
| 字段 | 类型 | 说明 |
|---|---|---|
user | SelfProfile | 本人资料:username、nickname、avatarUrl、attributes、hasPassword、version,见用户、好友与在线状态 |
config | ClientConfig | 应用的运行配置,与 im.config 相同 |
sessionId | String | 本次登录的会话 ID |
CurrentUser
| 字段 | 类型 | 说明 |
|---|---|---|
username | String | 用户名(小写) |
sessionId | String | 本次登录的会话 ID |
AuthState
密封类,用 switch 或 is 区分;每种都有 kind。
| 类型 | kind | 其他字段 | 说明 |
|---|---|---|---|
SignedOut | signed_out | 未登录 | |
SignedIn | signed_in | 已登录 | |
Suspended | suspended | code:String(tenant_unavailable 或 app_unavailable);reason:String?;retryAt:int | 服务暂停,见服务暂停 |
SessionEnded | ended | code:String;reason:String?;details:Map<String, Object?>? | 会话已结束,见会话结束 |
DRConnectionState
密封类,用 switch 或 is 区分;每种都有 kind。
| 类型 | kind | 其他字段 | 说明 |
|---|---|---|---|
ConnectionIdle | idle | 未登录、未开始连接,或在后台不连接 | |
ConnectionConnecting | connecting | 正在连接 | |
ConnectionAuthenticating | authenticating | 正在认证 | |
ConnectionSyncing | syncing | pending:List<SyncModule> | 正在同步,pending 为还没完成的模块:config、me、friends、groups、messages、push、calls、chatrooms、presence |
ConnectionReady | ready | 已就绪 | |
ConnectionWaiting | waiting | retryAt:int;lastErrorCode:String?;lastErrorReason:String? | 等待重连 |
ConnectionOffline | offline | 网络不可用 | |
ConnectionStopped | stopped | code:String(upgrade_required、replaced_elsewhere、invalid_auth);details:Map<String, Object?>? | 已停止,见连接状态 |
TicketContext
ticketProvider 的参数。
| 字段 | 类型 | 说明 |
|---|---|---|
reason | String | invalid_credentials:凭证登录失败;session_expired:登录已过期,静默重新登录 |
username | String? | 登录过期时为原来的用户名;凭证登录失败时为调用 loginWithTicket 时传入的 username,没有传时为 null |
OutboxOptions
| 字段 | 类型 | 说明 |
|---|---|---|
maxAge | Duration | 未发出的消息最多保留多久,超过的标为发送失败,默认 48 小时 |
persistAttachmentMaxBytes | int | 随发送队列复制保存的附件的上限,默认 200 MB;超过的只记下原文件的路径 |
MediaOptions
| 字段 | 类型 | 说明 |
|---|---|---|
imageMaxSide | int | 发送图片前压缩的长边,单位为像素,默认 2048 |
jpegQuality | int | 压缩时的 JPEG 质量,默认 85 |
imageCacheBytes | int | DRImage 的磁盘缓存上限,默认 200 MB |
诊断信息
im.connection.diagnostics() 返回的 Map 的键:
| 键 | 类型 | 说明 |
|---|---|---|
sdkVersion | String | SDK 的版本号 |
deviceId | String | 设备标识 |
sessionId | String? | 当前登录的会话 ID |
connectionId | String? | 当前长连接的 ID |
connectionState | String | 连接状态的 kind |
recentDisconnects | List | 最近 20 次断开,每项有 at(毫秒时间戳)、close_code、code、reason |
recentFailures | List | 最近 20 个失败的请求,每项有 at、op(请求名)、code、request_id |
loginMethod | String? | 登录方式:ticket 或 password |
lastSyncAt | Map | 各模块最近一次同步完成的时间(毫秒时间戳) |
ClientConfig
应用的运行配置,字段与服务端客户端登录响应中的 config 相同,以 camelCase 的属性提供,常用字段见运行配置;raw 为原始的 JSON,version(String)为配置的版本。
LogOptions
| 字段 | 类型 | 说明 |
|---|---|---|
level | LogLevel | 输出的最低级别:error、warn(默认)、info、debug |
sink | void Function(LogEntry)? | 输出函数,默认用 print 输出 |
LogEntry
| 字段 | 类型 | 说明 |
|---|---|---|
level | LogLevel | 级别 |
time | int | 毫秒时间戳 |
module | String | SDK 内部的模块名,如 connection、auth、messages |
message | String | 日志内容 |
code | String? | 错误码 |
reason | String? | 错误原因 |
requestId | String? | 请求的 request_id |
connectionId | String? | 长连接的 ID |
BackgroundCall
backgroundClient.getCall() 的结果。
| 字段 | 类型 | 说明 |
|---|---|---|
callId | String | 通话 ID |
status | String | ringing(振铃中)、ongoing(进行中)、ended(已结束) |
media | String? | audio 或 video |
caller | String? | 主叫的用户名 |
self | Map<String, Object?>? | 本人在通话中的状态 |
raw | Map<String, Object?> | 原始的 JSON |
频道能力与登录生命周期
运行配置新增 rtc_channels_enabled、rtc_channels_protocol_version,当前频道协议版本为 1;它们与客户端 channels.supported 分别表示服务端配置和本端媒体能力。旧 SDK 不因开启配置自动获得频道 API。频道使用当前 IM 登录身份,通过 HTTP 控制,网关连接只加快状态通知。
退出登录或更换登录时,SDK 会清理旧频道媒体;不能用新身份恢复旧会话。路由 / Widget 离开时仍需主动 leave,不能只释放视频元素。接入方式见独立频道。
