離線推送
App 在前臺時,訊息、好友和群的通知、來電都經長連線實時送達。App 切到後臺、鎖屏或被殺之後,長連線不再可用,IM 服務改為經手機的推送通道送達:iOS 經 Apple 推送(APNs),Android 經 Google FCM 或國內手機廠商的推送通道(華為、榮耀、小米、OPPO、vivo、魅族)。使用者點選通知後開啟 App,由你跳轉到對應的會話或頁面。
推送令牌的獲取、登記、重新整理和登出都由 Flutter SDK 完成。你需要做的是:
- 在控制檯中為每個通道上傳推送憑據;
- 在 App 中依賴推送包(和需要的廠商通道包),做好各平臺的配置;
- 建立客戶端時在
DRPushOptions中給出各通道的憑據名稱; - 請求通知許可權,處理使用者點選通知。
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();推送的內容、哪些裝置會收到、免打擾和角標的規則,見服務端文件離線推送。使用者的免打擾、預覽方式等設定見提醒、免打擾與舉報,來電見音視訊通話。
| 要做什麼 | 看哪裡 |
|---|---|
| 第一次接入推送 | 準備工作,再按平臺看 iOS 的配置、Android:FCM、國內廠商通道 |
| 通知不顯示或響鈴不對(Android 8 及以上) | Android 的通知渠道 |
| 點選通知後開啟對應的會話 | 處理通知的點選 |
| 設定頁的“接收新訊息通知”開關 | App 內的通知開關 |
| 推送的語言、角標、清除通知 | 推送的語言、角標、清除通知 |
| 在 FCM 後臺處理函式中呼叫 IM 服務 | 在後臺處理推送 |
| 收不到推送 | 檢查登記與排查 |
工作原理
- 線上時走長連線: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_flutter: ^1.0.0-alpha.4
deeprespond_im_push: ^1.0.0-alpha.4
# Android 使用 FCM 时,App 要直接调用这两个包
firebase_core: ^4.15.0
firebase_messaging: ^16.7.0
# 需要的国内厂商通道(只用于 Android)
deeprespond_im_push_huawei: ^1.0.0-alpha.4
deeprespond_im_push_xiaomi: ^1.0.0-alpha.4推送包依賴即生效:建立客戶端時只要給出了 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_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_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_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?>? | 本人在通話中的狀態 |
