离线推送
App 在前台时,消息、好友和群的通知、来电都经长连接实时送达。App 切到后台、锁屏或被杀之后,长连接不再可用,IM 服务改为经手机的推送通道送达:iOS 经 Apple 推送(APNs),Android 经 Google FCM 或国内手机厂商的推送通道(华为、荣耀、小米、OPPO、vivo、魅族)。用户点击通知后打开 App,由你跳转到对应的会话或页面。
推送令牌的获取、登记、刷新和注销都由 Flutter SDK 完成。你需要做的是:
- 在控制台中为每个通道上传推送凭据;
- 在 App 中依赖推送包(和需要的厂商通道包),做好各平台的配置;
- 创建客户端时在
DRPushOptions中给出各通道的凭据名称; - 请求通知权限,处理用户点击通知。
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:deeprespond_im_push/deeprespond_im_push.dart';
final im = await DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
push: const DRPushOptions(credentials: {
PushChannel.apns: 'ios-prod', // 控制台中推送凭据的名称
PushChannel.fcm: 'fcm-global',
}),
));
// 登录后请求通知权限,授权后 SDK 自动登记这台设备
await DRPush.of(im)?.requestPermission();推送的内容、哪些设备会收到、免打扰和角标的规则,见服务端文档离线推送。用户的免打扰、预览方式等设置见提醒、免打扰与举报,来电见音视频通话。
工作原理
- 在线时走长连接:App 在前台时,IM 服务不推送,消息由长连接收到,SDK 发出
MessageReceived等事件。 - 切到后台时上报:App 切到后台时,SDK 告诉服务端“这台设备在后台”,并上报当前的角标数;之后的消息、通知和来电都推送到这台设备。回到前台时 SDK 重新连接并同步,离线期间的消息不会丢失。
- 令牌的登记:登录成功、每次冷启动、令牌刷新、系统语言变化、用户重新打开通知、回到前台时发现通知权限有变化,SDK 都会提交这台设备的完整登记。令牌没有变化时重复提交没有影响。
- 令牌跟随登录:退出登录、被踢下线、会话过期后,服务端自动解绑这台设备,SDK 同时清除通知栏中本 App 的通知。再次登录后 SDK 重新登记。
- 一台设备最多 3 个令牌:Android 上,这台手机能用的厂商通道排在前面,FCM 排在最后作备用;服务端发送时使用第一个可用的,同一条推送不会弹出两次。iOS 使用 APNs,接入了系统来电界面的另外登记一个 VoIP 令牌,只用于来电。
准备工作
在控制台配置推送凭据
在控制台的推送配置中,为 App 用到的每个通道添加一份推送凭据,记下凭据的名称(如 ios-prod、fcm-global、xiaomi-main)。App 登记令牌时用这个名称指明凭据。
- 凭据中的 Bundle ID 或包名必须与 App 一致。
- APNs 分生产和开发两种环境,令牌不通用:从 Xcode 直接安装的调试版使用开发环境,App Store、TestFlight 的版本使用生产环境。请各上传一份凭据,App 按构建类型使用对应的名称(见下面的例子)。
- 一个 IM 应用对应多个客户端 App(如主 App 和极速版)时,各上传一份凭据,各个 App 填自己的凭据名称。
添加依赖
dependencies:
deeprespond_im: ^2.0.0
deeprespond_im_flutter: ^2.0.0
deeprespond_im_push: ^2.0.0
# Android 使用 FCM 时,App 要直接调用这两个包
firebase_core: ^4.15.0
firebase_messaging: ^16.7.0
# 需要的国内厂商通道(只用于 Android)
deeprespond_im_push_huawei: ^2.0.0
deeprespond_im_push_xiaomi: ^2.0.0推送包依赖即生效:创建客户端时只要给出了 push 选项,SDK 就自动登记推送。没有给出 push 选项的客户端不登记推送,DRPush.of(im) 为 null。
创建客户端时给出凭据名称
DRPushOptions.credentials 按通道给出凭据名称,没有给出的通道不登记。iOS 的开发版和生产版使用不同的 APNs 凭据:
import 'package:flutter/foundation.dart';
final pushOptions = DRPushOptions(
credentials: {
// 从 Xcode 安装的调试版用开发环境的凭据,发布版用生产环境的
PushChannel.apns: kReleaseMode ? 'ios-prod' : 'ios-dev',
PushChannel.fcm: 'fcm-global',
PushChannel.huawei: 'huawei-main',
PushChannel.xiaomi: 'xiaomi-main',
},
androidChannels: const [
AndroidChannel(id: 'im_message', name: '消息', kind: AndroidChannelKind.message),
AndroidChannel(id: 'im_call', name: '来电', kind: AndroidChannelKind.call),
AndroidChannel(id: 'im_silent', name: '不响铃', kind: AndroidChannelKind.silent),
],
);androidChannels 是 Android 的通知渠道,见 Android 的通知渠道。
请求通知权限
iOS 和 Android 13 及以上,App 要得到用户授权才能显示通知。没有授权时 SDK 不登记普通的推送令牌,这台设备收不到推送。请在合适的时机(如登录后、用户点击“开启通知”时)请求:
final push = DRPush.of(im);
if (push != null) {
final status = await push.permission();
if (status == PushPermission.notDetermined) {
final result = await push.requestPermission(); // 弹出系统的授权提示,授权后 SDK 自动登记
if (result == PushPermission.denied) showToast('请在系统设置中允许通知,否则收不到新消息提醒');
} else if (status == PushPermission.denied) {
showToast('通知已关闭,请在系统设置中开启');
}
}provisional是 iOS 的“临时授权”(通知静默地出现在通知中心),SDK 同样登记推送。- 用户之后在系统设置中关闭或重新打开通知,SDK 在 App 回到前台时发现并重新登记:关闭后注销这台设备的推送,避免服务端继续推送、累加角标;打开后重新登记。
- Android 12 及以下的通知默认允许,
permission()直接为granted。
iOS 的配置
在 Xcode 中打开 ios/Runner.xcworkspace,在 Runner 目标的 Signing & Capabilities 中添加:
- Push Notifications;
- Background Modes,勾选 Remote notifications。使用音视频通话的另外勾选 Voice over IP 和 Audio, AirPlay, and Picture in Picture,见音视频通话。
<!-- ios/Runner/Info.plist,与上面勾选的后台模式对应 -->
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>voip</string>
<string>audio</string>
</array>APNs 的令牌由 SDK 直接从系统取得,不经过 Firebase,iOS 上不需要 GoogleService-Info.plist。
App 在前台时的通知
App 在前台时,长连接已经收到了消息。SDK 默认不展示本 App 的消息、好友和群的通知(DRPushOptions.suppressInForeground 为 true),避免重复提醒;其他通知(如测试推送)照常展示。需要在前台也展示系统通知的,设为 false。
通知扩展(可选)
推送的标题使用服务端的昵称,不使用接收者设置的好友备注和群昵称。需要按本地备注改写通知、或下载图片显示在通知中的,可以自己添加 Notification Service Extension:
- 在控制台中为 APNs 凭据开启“允许通知扩展改写内容”(
mutable_content); - 在 Xcode 中添加 Notification Service Extension 目标;扩展需要读取 App 的数据(如备注表)时,为 App 和扩展添加同一个 App Groups,App 把数据写入共享的
UserDefaults,扩展从中读取。
推送中 IM 服务的数据在顶层键 im 中,结构见 PushPayload。扩展的内存和时间都很有限,SDK 不提供扩展的实现。
Android:FCM
FCM 适用于有 Google 服务的手机。没有 Google 服务的手机(国内销售的大多数手机)取不到 FCM 令牌,SDK 自动跳过,请同时接入国内厂商通道。
添加 Firebase 配置
- 在 Firebase 控制台的项目中添加 Android App(包名与 App 一致),下载
google-services.json,放到android/app/下; - 添加 Google Services 的 Gradle 插件(版本以 Firebase 文档为准):
// android/settings.gradle.kts
plugins {
// ...
id("com.google.gms.google-services") version "4.4.2" apply false
}// android/app/build.gradle.kts
plugins {
id("com.android.application")
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}- 在控制台中上传同一个 Firebase 项目的服务账号 JSON,作为 FCM 凭据。
初始化与后台处理函数
创建客户端之前调用 Firebase.initializeApp(),并登记 FCM 的后台处理函数。App 不在前台时(在后台、锁屏或被杀),FCM 的数据消息在单独的 isolate 中交给这个函数处理;来电就是这样送达的。在你的处理函数中调用 drFirebaseBackgroundHandler(message):
import 'dart:io';
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:deeprespond_im_push/deeprespond_im_push.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
/// FCM 的后台处理函数:必须是顶层函数,并标注 vm:entry-point
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp();
await drFirebaseBackgroundHandler(message);
// 你自己的其他数据消息在这里处理
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
}
final im = await DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
push: const DRPushOptions(credentials: {
PushChannel.apns: 'ios-prod',
PushChannel.fcm: 'fcm-global',
}),
));
runApp(MyApp(im: im));
}
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.im});
final DRClient im;
@override
Widget build(BuildContext context) => const MaterialApp(home: Placeholder());
}drFirebaseBackgroundHandler只处理 IM 服务的来电类消息(来电、未接、取消、清除来电通知),显示或关闭来电通知;其他消息直接返回,你可以在它之后处理自己的数据消息。- 消息和通知类的推送是 FCM 的通知消息,App 在后台时由系统直接显示,不经过这个函数。
- App 在前台时,FCM 的通知消息不由系统显示(长连接已经收到了);来电类的数据消息由 SDK 处理。
Android:国内厂商通道
国内销售的手机大多没有 Google 服务,要接入手机厂商自己的推送通道。每个厂商一个包,只用于 Android:
| 通道 | 包 | 在 main() 中调用 | 厂商推送 SDK | 适用的手机 |
|---|---|---|---|---|
| 华为 | deeprespond_im_push_huawei | registerHuaweiPush() | 随包自动引入 | 华为、2021 年以前的荣耀,以及装了 HMS Core 的手机 |
| 荣耀 | deeprespond_im_push_honor | registerHonorPush() | 随包自动引入 | 2021 年及以后的荣耀 |
| 小米 | deeprespond_im_push_xiaomi | registerXiaomiPush(appId: ..., appKey: ...) | App 放入 AAR | 小米、Redmi、POCO |
| OPPO | deeprespond_im_push_oppo | registerOppoPush(appKey: ..., appSecret: ...) | App 放入 AAR | OPPO、一加、realme |
| vivo | deeprespond_im_push_vivo | registerVivoPush() | App 放入 AAR | vivo、iQOO |
| 魅族 | deeprespond_im_push_meizu | registerMeizuPush(appId: ..., appKey: ...) | 随包自动引入 | 魅族 |
在 main() 中、创建客户端之前调用注册函数,并在 credentials 中给出对应通道的凭据名称:
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:deeprespond_im_push_honor/deeprespond_im_push_honor.dart';
import 'package:deeprespond_im_push_huawei/deeprespond_im_push_huawei.dart';
import 'package:deeprespond_im_push_meizu/deeprespond_im_push_meizu.dart';
import 'package:deeprespond_im_push_oppo/deeprespond_im_push_oppo.dart';
import 'package:deeprespond_im_push_vivo/deeprespond_im_push_vivo.dart';
import 'package:deeprespond_im_push_xiaomi/deeprespond_im_push_xiaomi.dart';
import 'package:flutter/widgets.dart';
Future<DRClient> createClient() async {
WidgetsFlutterBinding.ensureInitialized();
registerHuaweiPush();
registerHonorPush();
registerXiaomiPush(appId: '<小米 AppID>', appKey: '<小米 AppKey>');
registerOppoPush(appKey: '<OPPO AppKey>', appSecret: '<OPPO AppSecret>');
registerVivoPush();
registerMeizuPush(appId: '<魅族 AppID>', appKey: '<魅族 AppKey>');
return DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
push: const DRPushOptions(credentials: {
PushChannel.huawei: 'huawei-main',
PushChannel.honor: 'honor-main',
PushChannel.xiaomi: 'xiaomi-main',
PushChannel.oppo: 'oppo-main',
PushChannel.vivo: 'vivo-main',
PushChannel.meizu: 'meizu-main',
PushChannel.fcm: 'fcm-global',
}),
));
}- 用哪个通道:SDK 先问厂商 SDK 这台手机能否使用它的通道,厂商 SDK 无法判断时按手机品牌判断。能用的厂商通道排在前面,有 Google 服务的手机再加上 FCM 作备用。App 没有依赖的、没有给出凭据名称的通道跳过。
- 隐私合规:厂商 SDK 在用户登录后、登记推送时才初始化和取令牌,此前不调用。请在 App 的隐私政策中写明使用了哪些厂商的推送 SDK。
- 密钥只给服务端:注册函数中的 AppID、AppKey 是客户端参数;AppSecret、MasterSecret 等密钥只在控制台的推送凭据中填写,不要写进 App。
- 消息分类:国内厂商要求按内容申请消息分类(如即时通讯),没有申请的推送会被限量甚至拒收。请在厂商后台申请后填写在控制台的凭据中,见推送配置。
- 来电:国内厂商通道上的来电是普通的高优先级通知,用户点击后进入 App,见音视频通话。
华为
Maven 仓库:包已把华为的仓库
https://developer.huawei.com/repo/加到全部 Gradle 项目上。App 在android/settings.gradle.kts中用dependencyResolutionManagement统一管理仓库的,要在那里加上:kotlindependencyResolutionManagement { repositories { maven { url = uri("https://developer.huawei.com/repo/") } } }App ID:在华为 AppGallery Connect 中开通推送服务、配置签名证书的 SHA-256 指纹,下载
agconnect-services.json,放进android/app/src/main/assets/(不需要华为的 Gradle 插件);或者在AndroidManifest.xml的<application>中声明:xml<meta-data android:name="com.huawei.hms.client.appid" android:value="appid=<App ID>" />点击通知:点击通知时打开的是 scheme 为
impush的 Intent,App 的启动 Activity(通常是MainActivity)要声明:xml<activity android:name=".MainActivity" ...> <!-- 原有的 intent-filter 保留 --> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="impush" /> </intent-filter> </activity>
令牌刷新的服务、混淆规则由包自带,App 不需要另外声明。
荣耀
Maven 仓库:同华为,统一管理仓库的 App 加上
https://developer.hihonor.com/repo。App ID:在荣耀开发者服务平台开通推送服务,在
AndroidManifest.xml的<application>中声明:xml<meta-data android:name="com.hihonor.push.app_id" android:value="<App ID>" />点击通知:与华为相同,启动 Activity 声明 scheme 为
impush的 intent-filter。
2021 年以前的荣耀手机不支持荣耀推送,用的是华为的 HMS Core:同时依赖华为包的 App 在这些手机上走华为通道。
小米
小米推送 SDK 不在公开的 Maven 仓库上,需要 App 自己放入;没有放入时这个通道不可用,不影响其他通道。
放入 SDK:从小米开放平台下载 Android 客户端 SDK(AAR),放进
android/app/libs/:kotlin// android/app/build.gradle.kts dependencies { implementation(files("libs/MiPush_SDK_Client_x_x_x-C_3rd.aar")) }接收器:小米文档要求 App 声明一个继承
com.xiaomi.mipush.sdk.PushMessageReceiver的接收器,建议照做(空的子类即可):xml<receiver android:name=".MiPushReceiver" android:exported="true"> <intent-filter><action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" /></intent-filter> <intent-filter><action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" /></intent-filter> <intent-filter><action android:name="com.xiaomi.mipush.ERROR" /></intent-filter> </receiver>registerXiaomiPush的appId、appKey为小米开放平台中 App 的 AppID、AppKey。
第一次注册较慢时,令牌可能在登录后稍晚才取到,SDK 取到后自动登记。
OPPO
OPPO 推送 SDK 同样由 App 放入(适用于 OPPO、一加、realme 手机)。
放入 SDK:从 OPPO 开放平台下载推送 SDK(AAR),放进
android/app/libs/,在android/app/build.gradle.kts中implementation(files("libs/<OPPO 推送 SDK>.aar"))。较早的版本还要求 gson 等库,按所下载版本的 OPPO 文档添加。权限和服务:OPPO 的 AAR 没有声明它们,App 在
AndroidManifest.xml中声明:xml<uses-permission android:name="com.coloros.mcs.permission.RECIEVE_MCS_MESSAGE" /> <uses-permission android:name="com.heytap.mcs.permission.RECIEVE_MCS_MESSAGE" /> <application> <service android:name="com.heytap.msp.push.service.CompatibleDataMessageCallbackService" android:exported="true" android:permission="com.coloros.mcs.permission.SEND_MCS_MESSAGE"> <intent-filter> <action android:name="com.coloros.mcs.action.RECEIVE_MCS_MESSAGE" /> </intent-filter> </service> <service android:name="com.heytap.msp.push.service.DataMessageCallbackService" android:exported="true" android:permission="com.heytap.mcs.permission.SEND_PUSH_MESSAGE"> <intent-filter> <action android:name="com.heytap.mcs.action.RECEIVE_MCS_MESSAGE" /> <action android:name="com.heytap.msp.push.RECEIVE_MCS_MESSAGE" /> </intent-filter> </service> </application>registerOppoPush的appKey、appSecret为 OPPO 开放平台中的 AppKey、AppSecret(不是 MasterSecret,MasterSecret 只填在控制台中)。
vivo
vivo 推送 SDK 同样由 App 放入(适用于 vivo、iQOO 手机)。
放入 SDK:从 vivo 开放平台下载推送 SDK(AAR),放进
android/app/libs/,在android/app/build.gradle.kts中implementation(files("libs/vivo_pushSDK_v4.x.x.x_xxx.aar"))。AppID、AppKey:vivo SDK 从
AndroidManifest.xml读取,在<application>中声明:xml<meta-data android:name="com.vivo.push.api_key" android:value="<AppKey>" /> <meta-data android:name="com.vivo.push.app_id" android:value="<AppID>" />接收器:vivo 文档要求声明一个继承
com.vivo.push.sdk.OpenClientPushMessageReceiver的接收器,建议照做(空的子类即可):xml<receiver android:name=".VivoPushReceiver" android:exported="false"> <intent-filter> <action android:name="com.vivo.pushclient.action.RECEIVE" /> </intent-filter> </receiver>
魅族
魅族推送 SDK 随包从 Maven Central 引入,接收订阅结果的接收器、权限和混淆规则都由包声明,App 只需调用 registerMeizuPush(appId: ..., appKey: ...),参数为魅族开放平台中 App 的 AppID、AppKey。
Android 的通知渠道
Android 8 及以上,通知必须属于一个通知渠道,渠道不存在时通知可能不显示,或者按默认渠道响铃。在 DRPushOptions.androidChannels 中给出要创建的渠道,SDK 在创建客户端时创建它们:
用途 kind | 用于 | 重要级别 | 控制台中对应的凭据参数 |
|---|---|---|---|
AndroidChannelKind.message | 新消息、好友和群的通知 | 默认(有声音) | FCM 的 android_channel_id;小米、OPPO 的 channel_id |
AndroidChannelKind.call | 来电通知(全屏来电通知使用这个渠道) | 高 | 无 |
AndroidChannelKind.silent | 撤回后替换的通知、未接和取消的通知 | 低 | FCM、小米、OPPO 的 silent_channel_id |
- 渠道的
id必须与控制台中凭据参数填写的一致。华为、vivo 按消息分类(category)、荣耀按重要级别(importance)决定通知的样式,不使用渠道。 - 没有给出
call渠道时,来电包自己创建一个名为“来电”的高重要级别渠道。 - 渠道创建后,名称可以修改(下次创建客户端时更新),重要级别由用户在系统设置中决定,SDK 不覆盖。
处理通知的点击
用户点击通知打开 App 时,SDK 解析通知中 IM 服务的数据,以 PushOpened 事件和 DRPush.onOpened 交给你。按载荷的类型跳转:
void handlePush(PushPayload payload) {
switch (payload) {
case MessagePush(:final conversationType, :final conversationId, :final seq):
openConversation(conversationType, conversationId, seq: seq); // 打开会话,按 seq 定位到这条消息
case NoticePush(:final notice, :final groupId):
if (notice == 'friend.request') {
openFriendRequests();
} else if (groupId != null) {
openGroup(groupId);
}
// 预览方式为 none 时没有这些字段,只打开 App 即可
case CallPush(:final callId, :final event):
if (event == 'incoming') unawaited(openCall(callId)); // 显示来电界面,见“音视频通话”
case RecallPush() || DismissPush() || TestPush() || UnknownPush():
break; // 打开 App 即可
}
}
final push = DRPush.of(im);
// 冷启动由点击通知引起的:创建客户端后读取
final launch = push?.launchPayload;
if (launch != null) handlePush(launch);
// App 运行中点击通知
push?.onOpened.listen(handlePush);- 定位到消息:
MessagePush的conversationId就是会话列表中的键,seq是这条消息在会话中的序号。打开会话的消息列表时给出aroundSeq,列表直接定位到这条消息附近(已打开的列表用jumpTo(seq)),见消息:
final seq = payload.seq;
final list = seq == null
? im.messages.open(payload.conversationId)
: im.messages.open(payload.conversationId, aroundSeq: seq);- 冷启动:App 被杀后由点击通知启动的,SDK 在创建客户端的过程中就取得了载荷,
launchPayload可以一直读到。请在创建客户端后读取一次,不要只依赖onOpened。 im.on<PushOpened>()与onOpened是同一件事,用哪个都可以。MessagePush的ext是发送消息时你的服务端在push.ext中给出的自定义数据,原样交给你,见发送消息时的 push 字段。NoticePush的notice是通知模板的键,如friend.request、group.invitation,全部取值见控制台的模板键。- 在后台点击来电通知时的处理见音视频通话。
App 内的通知开关
App 的设置页中可以提供“接收新消息通知”的开关,关闭后这台设备不再收到推送(服务端注销这台设备的登记),打开后重新登记:
// 开关的当前值(保存在本机,重新安装后恢复为打开)
final on = push.enabled;
// 用户切换开关
await push.setEnabled(false);- 这个开关只影响这台设备。按时段或按会话的免打扰保存在服务端、在用户的全部设备间同步,见提醒、免打扰与举报。
- iOS 接入了系统来电界面的,关闭通知后仍然保留 VoIP 令牌,来电不受影响。
推送的语言
推送的标题和正文由服务端按设备的语言生成。SDK 默认提交系统的语言,系统语言变化时自动重新登记。App 内可以切换语言的,在用户切换后设置:
push.language = 'en-US'; // BCP 47 语言标签
push.language = null; // 恢复为系统的语言角标
App 切到后台时,SDK 把当前应该显示的角标数上报给服务端,之后每推送一条消息,服务端在这个数上加一并随通知下发(APNs、FCM 支持角标,国内厂商通道不带角标)。
- 默认上报未读总数,不计免打扰的会话(同
im.conversations.getUnreadTotal(excludeMuted: true))。要计入免打扰的会话,或加上 App 自己的未读数,在创建客户端时给出badgeProvider:
final options = DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
badgeProvider: () => im.conversations.getUnreadTotal() + myOwnUnread(),
push: const DRPushOptions(credentials: {PushChannel.apns: 'ios-prod'}),
);- 应用的运行策略关闭了角标(
push_badge_enabled为false)时,SDK 不上报,推送中也不带角标。 - SDK 不修改 App 图标上的角标。App 回到前台后,请按未读数自行更新图标的角标(或清零),下次切到后台时 SDK 重新上报。
清除通知
- 退出登录、被踢下线、会话过期时,SDK 自动清除通知栏中本 App 的全部通知。
- 打开一个会话后,可以调用
clearDeliveredNotifications()清除本 App 已显示的通知;cancelNotification(tag)取消一条通知,如以通话 ID 为 tag 的来电通知。
在后台处理推送
FCM 的后台处理函数、或 App 自己的后台任务运行在单独的 isolate 中,不能创建完整的客户端。需要在那里调用 IM 服务时,使用轻量的后台客户端 DRBackgroundClient:它不建立长连接、不读写本地数据库,只提供处理推送所需的少数操作(解析载荷、查询通话、接听和拒绝来电、上报角标)。App 的主界面在运行时,这些操作转给主 isolate 中的客户端执行。
import 'package:deeprespond_im/deeprespond_im.dart';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:deeprespond_im_push/deeprespond_im_push.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp();
await drFirebaseBackgroundHandler(message);
// 你自己的数据消息:如服务端要求客户端更新角标
final badge = int.tryParse(message.data['my_badge']?.toString() ?? '');
if (badge == null) return;
final client = await DRBackgroundClient.open(appKey: '1575529652#demo', platform: DRFlutterPlatform.instance);
try {
await client.reportBadge(badge);
} finally {
await client.close();
}
}- 后台客户端使用主 isolate 中已登录的会话;从来没有在这台设备上创建过客户端的(不知道服务端地址),
open()抛出invalid_state(reason为no_launch_info)。 - 来电的处理(显示和关闭来电通知、忙线时拒绝)由
drFirebaseBackgroundHandler完成,不需要你用后台客户端处理。
检查登记与排查
currentRegistration() 返回服务端记录的这台设备的登记(令牌是遮盖过的),用于在调试页面上确认:
final device = await push.currentRegistration();
if (device == null) {
print('这台设备没有登记推送');
} else {
print('${device.platform} ${device.tokens} ${device.language}');
}收不到推送时:
- 确认
permission()为granted,enabled为true; - 确认
credentials中的凭据名称与控制台一致、凭据的包名与 App 一致、APNs 的环境与安装方式一致。凭据名称不存在、平台不符、令牌无效等是配置错误,SDK 不重试,在日志中记录一条错误(登记推送失败,请检查推送凭据的配置); - 在控制台中向这个用户发送测试推送,查看推送记录,见排查收不到推送。
接口参考
DRPush 由 deeprespond_im_push 包提供。返回 Future 的方法失败时抛出 DRException,错误码见事件与错误处理和服务端的错误码。
DRPush.of()
取得客户端的推送对象。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
client | DRClient | 是 | 客户端 |
返回值:DRPush?,创建客户端时没有给出 push 选项的为 null。
push.permission()
查询系统的通知权限。
返回值:Future<PushPermission>,见 PushPermission。
push.requestPermission()
请求通知权限:还没有询问过的弹出系统的授权提示,已经决定过的直接返回当前的权限。授权(或临时授权)后 SDK 立即登记这台设备。
返回值:Future<PushPermission>。
push.setEnabled()
App 内的通知开关。关闭时注销这台设备的推送(iOS 接入了系统来电界面的保留 VoIP 令牌),打开时重新登记。保存在本机。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
enabled | bool | 是 | true 打开,false 关闭 |
返回值:Future<void>。
push.enabled
只读属性,bool:App 内的通知开关,默认 true。
push.register()
立即重新提交这台设备的登记。SDK 在需要时自动登记,通常不需要调用。没有登录时什么也不做;遇到网络错误、服务端暂时故障时自动重试,配置错误不重试,都不抛出。
返回值:Future<void>。
push.currentRegistration()
查询服务端记录的这台设备的登记。
返回值:Future<PushDevice?>,没有登记时为 null。见 PushDevice。
可能的错误:not_signed_in、network_error、timeout。
push.clearDeliveredNotifications()
清除通知栏中本 App 已显示的全部通知。
返回值:Future<void>。
push.cancelNotification()
取消一条通知。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag | String | 是 | 通知的 tag,来电通知为通话 ID |
返回值:Future<void>。
push.onOpened
只读属性,Stream<PushPayload>:用户点击通知时发出点击带来的载荷,同时发出 PushOpened 事件。
push.launchPayload
只读属性,PushPayload?:冷启动由点击通知引起时的载荷;不是时为 null。
push.language
只写属性,String?:推送使用的语言(BCP 47,如 zh-CN、en-US),设置后 SDK 重新登记。null 为使用系统的语言。
registerHuaweiPush() 等
各厂商通道包的注册函数,在 main() 中、创建客户端之前调用。
| 函数 | 包 | 参数 |
|---|---|---|
registerHuaweiPush() | deeprespond_im_push_huawei | 无 |
registerHonorPush() | deeprespond_im_push_honor | 无 |
registerXiaomiPush({required String appId, required String appKey}) | deeprespond_im_push_xiaomi | 小米开放平台的 AppID、AppKey |
registerOppoPush({required String appKey, required String appSecret}) | deeprespond_im_push_oppo | OPPO 开放平台的 AppKey、AppSecret |
registerVivoPush() | deeprespond_im_push_vivo | 无(AppID、AppKey 写在 AndroidManifest.xml 中) |
registerMeizuPush({required String appId, required String appKey}) | deeprespond_im_push_meizu | 魅族开放平台的 AppID、AppKey |
返回值:无。在 iOS 上调用不产生任何作用。
drFirebaseBackgroundHandler()
处理 FCM 的后台数据消息中 IM 服务的来电类消息。在你用 FirebaseMessaging.onBackgroundMessage 登记的处理函数中调用。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
message | RemoteMessage | 是 | 处理函数收到的消息 |
返回值:Future<void>。不是 IM 服务的来电类消息时直接返回。
PushPayload.parse()
从推送的自定义数据中取出 IM 服务的 im 对象并解析。SDK 已经解析好点击带来的数据,通常不需要调用;在通知扩展、自己的推送处理中可以用它。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
data | Map<String, Object?> | 是 | 推送的自定义数据,如 FCM 的 RemoteMessage.data、APNs 的 userInfo |
channel | PushChannel? | 否 | 来源的通道 |
返回值:PushPayload?,不是 IM 服务的推送时为 null。
DRBackgroundClient.open()
在后台 isolate 中打开后台客户端,用完后调用 close()。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
appKey | String | 是(命名参数) | 应用的 AppKey |
platform | DRPlatform | 是(命名参数) | DRFlutterPlatform.instance |
apiUrl | Uri? | 否(命名参数) | 服务端地址,默认用主 isolate 创建客户端时保存的 |
log | LogOptions | 否(命名参数) | 日志选项 |
返回值:Future<DRBackgroundClient>。
可能的错误:invalid_state(no_launch_info,没有给出 apiUrl,这台设备上也没有创建过客户端)。
后台客户端的方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
parse(Map<String, Object?> data, [PushChannel? channel]) | PushPayload? | 同 PushPayload.parse() |
getCall(String callId) | Future<BackgroundCall?> | 查询通话;不存在或本人不在其中时为 null |
answerCall(String callId) | Future<void> | 接听来电(只接听,不连接媒体) |
rejectCall(String callId, {String reason = 'declined'}) | Future<void> | 拒绝来电,reason 为 declined 或 busy |
reportBadge(int badge) | Future<void> | 上报这台设备的角标数 |
close() | Future<void> | 关闭 |
失败时抛出 DRException,网络错误为 network_error、timeout。
数据结构
DRPushOptions
创建客户端时的推送选项(DRClientOptions.push)。
| 字段 | 类型 | 说明 |
|---|---|---|
credentials | Map<PushChannel, String> | 必填。各通道在控制台中的凭据名称;没有给出的通道不登记 |
voipCredential | String? | iOS 的 VoIP 令牌使用的凭据名称,默认与 apns 相同。只在接入了系统来电界面(DRCallOptions.useCallKit 为 true)时登记 |
androidChannels | List<AndroidChannel> | 要创建的 Android 通知渠道,默认为空 |
suppressInForeground | bool | iOS:App 在前台时不展示本 App 的消息、好友和群的通知,默认 true |
PushChannel
推送通道:apns、fcm、huawei、honor、xiaomi、oppo、vivo、meizu。
AndroidChannel
| 字段 | 类型 | 说明 |
|---|---|---|
id | String | 渠道 ID,与控制台中凭据参数填写的一致 |
name | String | 显示在系统设置中的名称 |
kind | AndroidChannelKind | 用途:message 消息和通知、call 来电、silent 不响铃 |
description | String? | 显示在系统设置中的说明 |
PushPermission
通知权限:granted 已授权、denied 已拒绝、notDetermined 还没有询问过、provisional iOS 的临时授权。
PushDevice
服务端记录的这台设备的登记。
| 字段 | 类型 | 说明 |
|---|---|---|
platform | String | 设备平台,如 ios、android |
tokens | List<Object?> | 登记的普通令牌(遮盖过),每项含凭据名称、通道等 |
voipToken | Map<String, Object?>? | iOS 的 VoIP 令牌(遮盖过),没有时为 null |
language | String? | 登记的语言 |
raw | Map<String, Object?> | 原始数据,字段见服务端的推送设备 |
PushPayload
推送中 IM 服务的数据,按 kind 分为以下几类(sealed class,可以用 switch 穷举)。各类都有 version(int,结构的版本)、kind(String)和 raw(Map<String, Object?>,原始的 im 对象)。不认识的字段忽略。
| 类 | kind | 说明 |
|---|---|---|
MessagePush | message | 新消息 |
RecallPush | recall | 撤回后替换的通知 |
NoticePush | notice | 好友和群的通知 |
CallPush | call | 来电、未接和取消 |
DismissPush | dismiss | 清除来电通知(本人在其他设备接听或拒绝) |
TestPush | test | 控制台发出的测试推送 |
UnknownPush | 其他 | 以后新增的类型,按普通通知处理,点击后打开 App 即可 |
MessagePush
| 字段 | 类型 | 说明 |
|---|---|---|
conversationType | String | single 或 group |
conversationId | String | 会话 ID |
seq | int? | 消息在会话中的序号,用于定位消息 |
messageId | String? | 消息 ID |
sender | String? | 发送者的用户名;预览方式为 none、已删除的用户和系统身份发送的为 null |
ext | Map<String, Object?>? | 发送时 push.ext 中的自定义数据 |
RecallPush
| 字段 | 类型 | 说明 |
|---|---|---|
conversationType | String | single 或 group |
conversationId | String | 会话 ID |
messageId | String? | 被撤回的消息 ID |
NoticePush
预览方式为 none 时三个字段都为 null,点击后只打开 App。
| 字段 | 类型 | 说明 |
|---|---|---|
notice | String? | 通知模板的键,如 friend.request、group.invitation |
user | String? | 相关的用户名 |
groupId | String? | 相关的群 ID |
CallPush
| 字段 | 类型 | 说明 |
|---|---|---|
event | String | incoming 来电、missed 未接、canceled 已取消 |
callId | String | 通话 ID |
callerName | String? | 主叫的昵称,没有昵称时为用户名;预览方式为 none 时为 null |
ringExpiresAt | DateTime? | 振铃的截止时间 |
title | String? | FCM 的来电类消息带的标题(已按设备语言生成) |
body | String? | 同上,正文 |
data | Map<String, Object?>? | 通话的数据:call_id、media、caller、caller_nickname、ring_expires_at 等;预览方式为 none 时为 null |
DismissPush
| 字段 | 类型 | 说明 |
|---|---|---|
callId | String | 要清除来电通知的通话 ID |
BackgroundCall
后台客户端 getCall() 的结果。
| 字段 | 类型 | 说明 |
|---|---|---|
callId | String | 通话 ID |
status | String | 通话状态:ringing、active、ended |
media | String? | audio 或 video |
caller | String? | 主叫的用户名 |
self | Map<String, Object?>? | 本人在通话中的状态 |
