Push notifications
While the app is in the foreground, messages, friend and group notices and incoming calls arrive live over the connection. Once the app goes to the background, the screen locks or the app is killed, the connection is gone and the IM service delivers through the phone's push channel instead: Apple Push (APNs) on iOS, and Google FCM or a Chinese phone maker's push channel (Huawei, Honor, Xiaomi, OPPO, vivo, Meizu) on Android. When the user taps a notification, the app opens and you take them to the right conversation or page.
The Flutter SDK gets, registers, refreshes and unregisters push tokens for you. What you do:
- Upload push credentials for each channel in the console.
- Add the push package (and any phone-maker channel packages) to the app and configure each platform.
- Give each channel's credential name in
DRPushOptionswhen creating the client. - Ask for notification permission and handle notification taps.
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();For what pushes contain, which devices get them, and the rules for Do Not Disturb and badges, see Push notifications in the server docs. For the user's Do Not Disturb and preview settings, see Alerts, Do Not Disturb and reports; for incoming calls, see Audio and video calls.
| You want to | See |
|---|---|
| Set up push for the first time | Setup, then per platform: iOS setup, Android: FCM, Chinese phone-maker channels |
| Fix notifications not showing or ringing wrongly (Android 8+) | Android notification channels |
| Open the right conversation when a notification is tapped | Handle notification taps |
| Add a "New message notifications" switch to settings | In-app notification switch |
| Set the push language, badges, clear notifications | Push language, Badges, Clear notifications |
| Call the IM service from the FCM background handler | Handle pushes in the background |
| Debug missing pushes | Check the registration and troubleshoot |
How it works
- Online, the connection is used. While the app is in the foreground the IM service doesn't push. Messages arrive over the connection and the SDK emits events such as
MessageReceived. - Going to the background is reported. When the app goes to the background, the SDK tells the server "this device is in the background" and reports the current badge count. From then on, messages, notices and calls are pushed to the device. When the app returns to the foreground, the SDK reconnects and syncs, so nothing from while it was away is lost.
- Registering the token. The SDK submits the device's full registration after a successful sign-in, on every cold start, when the token refreshes, when the system language changes, when the user turns notifications back on, and when the app returns to the foreground and finds the notification permission changed. Submitting again with an unchanged token has no effect.
- Tokens follow sign-in. On sign-out, being kicked offline or session expiry, the server unbinds the device and the SDK clears the app's notifications from the notification shade. The SDK registers again on the next sign-in.
- Up to 3 tokens per device. On Android, the phone-maker channels the phone can use come first and FCM comes last as a fallback. The server sends through the first usable one, so the same push never shows twice. iOS uses APNs; apps using the system call screen also register a VoIP token, used only for incoming calls.
Setup
Configure push credentials in the console
In the console's Push settings, add a push credential for each channel the app uses and note its name (such as ios-prod, fcm-global, xiaomi-main). The app names the credential with it when registering a token.
- The credential's Bundle ID or package name must match the app.
- APNs has separate production and development environments, and tokens don't work across them. Debug builds installed straight from Xcode use development; App Store and TestFlight builds use production. Upload one credential for each, and have the app use the matching name per build type (see the example below).
- If one IM app has several client apps (such as a main app and a lite version), upload a credential for each, and have each app give its own credential name.
Add dependencies
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.4Adding the push package is enough to turn it on: whenever the client is created with the push option, the SDK registers for push automatically. Clients created without push don't register, and DRPush.of(im) is null.
Give credential names when creating the client
DRPushOptions.credentials gives a credential name per channel; channels without one aren't registered. iOS debug and release builds use different APNs credentials:
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 are Android notification channels; see Android notification channels.
Ask for notification permission
On iOS and on Android 13 and later, the app needs the user's permission to show notifications. Without it the SDK doesn't register a regular push token and the device gets no pushes. Ask at a suitable moment, such as after sign-in or when the user taps "Turn on notifications":
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('通知已关闭,请在系统设置中开启');
}
}provisionalis iOS's provisional authorization (notifications are delivered quietly to Notification Center); the SDK registers for push in that case too.- If the user later turns notifications off or back on in system settings, the SDK notices when the app returns to the foreground. Turning them off unregisters the device so the server stops pushing and adding to the badge; turning them on registers again.
- On Android 12 and earlier notifications are allowed by default, and
permission()returnsgranted.
iOS setup
Open ios/Runner.xcworkspace in Xcode and add these to the Runner target under Signing & Capabilities:
- Push Notifications.
- Background Modes, with Remote notifications checked. Apps with audio and video calls also check Voice over IP and Audio, AirPlay, and Picture in Picture; see Audio and video calls.
<!-- ios/Runner/Info.plist,与上面勾选的后台模式对应 -->
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>voip</string>
<string>audio</string>
</array>The SDK gets the APNs token straight from the system, not through Firebase, so iOS doesn't need GoogleService-Info.plist.
Notifications while the app is in the foreground
While the app is in the foreground, the connection has already delivered the message. By default the SDK doesn't show the app's message, friend and group notifications (DRPushOptions.suppressInForeground is true) to avoid alerting twice; other notifications (such as test pushes) still show. Set it to false to show system notifications in the foreground too.
Notification extension (optional)
Push titles use the server-side nickname, not the friend remarks or group nicknames the recipient set. To rewrite notifications using local remarks, or to download an image into the notification, add your own Notification Service Extension:
- In the console, turn on "Allow notification extensions to modify content" (
mutable_content) for the APNs credential. - Add a Notification Service Extension target in Xcode. If the extension needs the app's data (such as the remarks table), add the same App Group to the app and the extension; the app writes the data to shared
UserDefaultsand the extension reads it from there.
The IM service's data is under the top-level key im in the push; for its structure, see PushPayload. Extensions have very little memory and time, and the SDK doesn't provide an extension implementation.
Android: FCM
FCM works on phones with Google services. Phones without them (most phones sold in China) can't get an FCM token and the SDK skips it, so also add the Chinese phone-maker channels.
Add the Firebase config
- In your Firebase project, add an Android app (with the app's package name), download
google-services.jsonand put it inandroid/app/. - Add the Google Services Gradle plugin (check the Firebase docs for the version):
// 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")
}- Upload a service account JSON from the same Firebase project in the console as the FCM credential.
Initialization and the background handler
Before creating the client, call Firebase.initializeApp() and register an FCM background handler. When the app isn't in the foreground (in the background, locked or killed), FCM data messages are handed to that function in a separate isolate; this is how incoming calls arrive. Call drFirebaseBackgroundHandler(message) in your handler:
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());
}drFirebaseBackgroundHandleronly handles the IM service's call messages (incoming, missed, cancelled, and clearing the call notification), showing or closing the call notification. It returns straight away for other messages, so you can handle your own data messages after it.- Message and notice pushes are FCM notification messages. In the background the system shows them directly, without going through this function.
- In the foreground, FCM notification messages aren't shown by the system (the connection already delivered them); call data messages are handled by the SDK.
Android: Chinese phone-maker channels
Most phones sold in China have no Google services, so you need each phone maker's own push channel. There's one package per maker, for Android only:
| Channel | Package | Call in main() | Maker's push SDK | Phones |
|---|---|---|---|---|
| Huawei | deeprespond_im_push_huawei | registerHuaweiPush() | Included by the package | Huawei, Honor before 2021, and phones with HMS Core |
| Honor | deeprespond_im_push_honor | registerHonorPush() | Included by the package | Honor from 2021 on |
| Xiaomi | deeprespond_im_push_xiaomi | registerXiaomiPush(appId: ..., appKey: ...) | The app adds the AAR | Xiaomi, Redmi, POCO |
| OPPO | deeprespond_im_push_oppo | registerOppoPush(appKey: ..., appSecret: ...) | The app adds the AAR | OPPO, OnePlus, realme |
| vivo | deeprespond_im_push_vivo | registerVivoPush() | The app adds the AAR | vivo, iQOO |
| Meizu | deeprespond_im_push_meizu | registerMeizuPush(appId: ..., appKey: ...) | Included by the package | Meizu |
Call the register functions in main(), before creating the client, and give each channel's credential name in 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',
}),
));
}- Which channel is used. The SDK asks each maker's SDK whether the phone can use its channel, falling back to the phone's brand when the maker's SDK can't tell. Usable maker channels come first, with FCM added as a fallback on phones with Google services. Channels the app doesn't depend on, or has no credential name for, are skipped.
- Privacy compliance. Maker SDKs are only initialized and asked for tokens when registering push after the user signs in, never before. List the maker push SDKs you use in the app's privacy policy.
- Secrets stay on the server. The AppID and AppKey in the register functions are client parameters. Secrets such as AppSecret and MasterSecret go only in the console's push credentials; don't put them in the app.
- Message categories. Chinese phone makers require you to apply for message categories by content (such as instant messaging); pushes without one may be throttled or rejected. Apply in the maker's console and enter the result in the console's credential; see Push settings.
- Incoming calls. Calls over Chinese maker channels are regular high-priority notifications, and the user taps one to enter the app; see Audio and video calls.
Huawei
Maven repository. The package adds Huawei's repository
https://developer.huawei.com/repo/to all Gradle projects. If the app manages repositories centrally withdependencyResolutionManagementinandroid/settings.gradle.kts, add it there:kotlindependencyResolutionManagement { repositories { maven { url = uri("https://developer.huawei.com/repo/") } } }App ID. In Huawei AppGallery Connect, turn on Push Kit, add the SHA-256 fingerprint of your signing certificate, download
agconnect-services.jsonand put it inandroid/app/src/main/assets/(Huawei's Gradle plugin isn't needed). Or declare it in<application>inAndroidManifest.xml:xml<meta-data android:name="com.huawei.hms.client.appid" android:value="appid=<App ID>" />Notification taps. Tapping a notification opens an Intent with the scheme
impush, so the app's launch Activity (usuallyMainActivity) must declare: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>
The token refresh service and ProGuard rules come with the package; the app doesn't declare them.
Honor
Maven repository. As with Huawei, apps that manage repositories centrally add
https://developer.hihonor.com/repo.App ID. Turn on push in the Honor developer platform and declare it in
<application>inAndroidManifest.xml:xml<meta-data android:name="com.hihonor.push.app_id" android:value="<App ID>" />Notification taps. Same as Huawei: the launch Activity declares an intent-filter for the
impushscheme.
Honor phones from before 2021 don't support Honor push and use Huawei's HMS Core instead: apps that also depend on the Huawei package use the Huawei channel on them.
Xiaomi
Xiaomi's push SDK isn't on a public Maven repository, so the app has to add it. Without it this channel is unavailable, which doesn't affect the others.
Add the SDK. Download the Android client SDK (AAR) from the Xiaomi open platform and put it in
android/app/libs/:kotlin// android/app/build.gradle.kts dependencies { implementation(files("libs/MiPush_SDK_Client_x_x_x-C_3rd.aar")) }Receiver. Xiaomi's docs require the app to declare a receiver extending
com.xiaomi.mipush.sdk.PushMessageReceiver. We recommend doing so (an empty subclass is fine):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>The
appIdandappKeyofregisterXiaomiPushare the app's AppID and AppKey on the Xiaomi open platform.
The first registration can be slow, so the token may arrive a little after sign-in; the SDK registers it as soon as it arrives.
OPPO
The app also adds OPPO's push SDK itself (for OPPO, OnePlus and realme phones).
Add the SDK. Download the push SDK (AAR) from the OPPO open platform, put it in
android/app/libs/, and addimplementation(files("libs/<OPPO push SDK>.aar"))inandroid/app/build.gradle.kts. Older versions also need libraries such as gson; add them as OPPO's docs for your version say.Permissions and services. OPPO's AAR doesn't declare them, so declare them in the app's
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>The
appKeyandappSecretofregisterOppoPushare the AppKey and AppSecret from the OPPO open platform (not the MasterSecret, which goes only in the console).
vivo
The app also adds vivo's push SDK itself (for vivo and iQOO phones).
Add the SDK. Download the push SDK (AAR) from the vivo open platform, put it in
android/app/libs/, and addimplementation(files("libs/vivo_pushSDK_v4.x.x.x_xxx.aar"))inandroid/app/build.gradle.kts.AppID and AppKey. The vivo SDK reads them from
AndroidManifest.xml; declare them in<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>" />Receiver. vivo's docs require declaring a receiver extending
com.vivo.push.sdk.OpenClientPushMessageReceiver. We recommend doing so (an empty subclass is fine):xml<receiver android:name=".VivoPushReceiver" android:exported="false"> <intent-filter> <action android:name="com.vivo.pushclient.action.RECEIVE" /> </intent-filter> </receiver>
Meizu
The Meizu push SDK comes with the package from Maven Central, and the package declares the subscription receiver, permissions and ProGuard rules. The app only calls registerMeizuPush(appId: ..., appKey: ...) with the app's AppID and AppKey from the Meizu open platform.
Android notification channels
On Android 8 and later every notification must belong to a notification channel. If the channel doesn't exist, the notification may not show or may ring with the default channel's settings. List the channels to create in DRPushOptions.androidChannels, and the SDK creates them when the client is created:
kind | Used for | Importance | Matching credential parameter in the console |
|---|---|---|---|
AndroidChannelKind.message | New messages, friend and group notices | Default (with sound) | FCM android_channel_id; Xiaomi and OPPO channel_id |
AndroidChannelKind.call | Incoming call notifications (full-screen call notifications use this channel) | High | None |
AndroidChannelKind.silent | Notifications replaced after a recall, missed and cancelled call notifications | Low | FCM, Xiaomi and OPPO silent_channel_id |
- A channel's
idmust match what's entered in the credential parameter in the console. Huawei and vivo decide notification style by message category (category), and Honor by importance (importance); they don't use channels. - If there's no
callchannel, the call package creates a high-importance channel named "来电" (Incoming calls) itself. - After a channel is created you can rename it (it updates the next time the client is created). Importance is up to the user in system settings, and the SDK doesn't override it.
Handle notification taps
When the user taps a notification to open the app, the SDK parses the IM service's data in it and gives it to you as a PushOpened event and through DRPush.onOpened. Navigate by payload type:
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);- Go to the message.
conversationIdin aMessagePushis the conversation list key, andseqis the message's number in the conversation. PassaroundSeqwhen opening the conversation's message list and it opens positioned at that message (usejumpTo(seq)on a list that's already open); see Messages:
final seq = payload.seq;
final list = seq == null
? im.messages.open(payload.conversationId)
: im.messages.open(payload.conversationId, aroundSeq: seq);- Cold start. When a tap launches the app after it was killed, the SDK picks up the payload while the client is being created, and
launchPayloadkeeps returning it. Read it once after creating the client; don't rely ononOpenedalone. im.on<PushOpened>()andonOpenedare the same thing; use either.extin aMessagePushis the custom data your server put inpush.extwhen sending the message, passed through as is; see The push field when sending messages.noticein aNoticePushis the notification template key, such asfriend.requestorgroup.invitation; for all values, see Template keys in the console docs.- For taps on incoming call notifications in the background, see Audio and video calls.
In-app notification switch
The app's settings page can offer a "New message notifications" switch. Turning it off stops pushes to this device (the server unregisters the device); turning it on registers again:
// 开关的当前值(保存在本机,重新安装后恢复为打开)
final on = push.enabled;
// 用户切换开关
await push.setEnabled(false);- The switch only affects this device. Do Not Disturb by time or by conversation is stored on the server and synced across all the user's devices; see Alerts, Do Not Disturb and reports.
- On iOS with the system call screen, the VoIP token is kept when notifications are turned off, so incoming calls still work.
Push language
The server writes push titles and text in the device's language. By default the SDK submits the system language and registers again when it changes. If users can switch language inside the app, set it after they switch:
push.language = 'en-US'; // BCP 47 语言标签
push.language = null; // 恢复为系统的语言Badges
When the app goes to the background, the SDK reports the badge count it should show to the server. From then on the server adds one for each pushed message and sends the count with the notification (APNs and FCM support badges; Chinese phone-maker channels don't carry them).
- By default the unread total is reported, not counting muted conversations (the same as
im.conversations.getUnreadTotal(excludeMuted: true)). To count muted conversations, or add the app's own unread count, givebadgeProviderwhen creating the client:
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'}),
);- When the app's runtime policy turns badges off (
push_badge_enabledisfalse), the SDK doesn't report them and pushes carry no badge. - The SDK doesn't change the badge on the app icon. When the app returns to the foreground, update (or clear) the icon badge from the unread count yourself; the SDK reports again the next time the app goes to the background.
Clear notifications
- On sign-out, being kicked offline or session expiry, the SDK clears all the app's notifications from the notification shade automatically.
- After opening a conversation you can call
clearDeliveredNotifications()to clear the app's shown notifications.cancelNotification(tag)cancels one notification, such as an incoming call notification tagged with the call ID.
Handle pushes in the background
The FCM background handler, and the app's own background tasks, run in a separate isolate and can't create a full client. To call the IM service there, use the lightweight background client DRBackgroundClient. It doesn't open a connection or touch the local database, and only offers the few operations push handling needs (parsing payloads, looking up calls, answering and rejecting calls, reporting the badge). While the app's main UI is running, these operations are forwarded to the client in the main 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();
}
}- The background client uses the signed-in session from the main isolate. If a client has never been created on this device (so the server address is unknown),
open()throwsinvalid_state(reasonno_launch_info). - Incoming calls (showing and closing the call notification, rejecting when busy) are handled by
drFirebaseBackgroundHandler; you don't need the background client for them.
Check the registration and troubleshoot
currentRegistration() returns the server's record of this device's registration (with tokens masked), so you can confirm it on a debug page:
final device = await push.currentRegistration();
if (device == null) {
print('这台设备没有登记推送');
} else {
print('${device.platform} ${device.tokens} ${device.language}');
}When pushes don't arrive:
- Check that
permission()isgrantedandenabledistrue. - Check that the credential names in
credentialsmatch the console, the credential's package name matches the app, and the APNs environment matches how the app was installed. A missing credential name, a platform mismatch or an invalid token are configuration errors: the SDK doesn't retry and logs an error (登记推送失败,请检查推送凭据的配置, "push registration failed, check the push credential configuration"). - Send a test push to the user from the console and look at the push records; see Troubleshoot missing pushes.
API reference
DRPush comes from the deeprespond_im_push package. Methods returning a Future throw DRException on failure; for error codes, see Events and errors and the server's Error codes.
DRPush.of()
Gets the client's push object.
| Parameter | Type | Required | Description |
|---|---|---|---|
client | DRClient | Yes | The client |
Returns: DRPush?; null if the client was created without the push option.
push.permission()
Gets the system notification permission.
Returns: Future<PushPermission>; see PushPermission.
push.requestPermission()
Asks for notification permission: shows the system prompt if the user hasn't been asked yet, otherwise returns the current permission. Once granted (or provisionally granted), the SDK registers the device right away.
Returns: Future<PushPermission>.
push.setEnabled()
The in-app notification switch. Turning it off unregisters the device (on iOS with the system call screen, the VoIP token is kept); turning it on registers again. Stored on the device.
| Parameter | Type | Required | Description |
|---|---|---|---|
enabled | bool | Yes | true to turn on, false to turn off |
Returns: Future<void>.
push.enabled
Read-only bool: the in-app notification switch, true by default.
push.register()
Submits the device's registration again right away. The SDK registers automatically when needed, so you usually don't call this. Does nothing when not signed in. It retries network errors and temporary server failures automatically, doesn't retry configuration errors, and never throws.
Returns: Future<void>.
push.currentRegistration()
Gets the server's record of this device's registration.
Returns: Future<PushDevice?>; null if not registered. See PushDevice.
Errors: not_signed_in, network_error, timeout.
push.clearDeliveredNotifications()
Clears all the app's shown notifications from the notification shade.
Returns: Future<void>.
push.cancelNotification()
Cancels one notification.
| Parameter | Type | Required | Description |
|---|---|---|---|
tag | String | Yes | The notification's tag; the call ID for incoming call notifications |
Returns: Future<void>.
push.onOpened
Read-only Stream<PushPayload>: emits the payload when the user taps a notification, together with a PushOpened event.
push.launchPayload
Read-only PushPayload?: the payload when a notification tap caused the cold start; otherwise null.
push.language
Write-only String?: the language for pushes (BCP 47, such as zh-CN or en-US). The SDK registers again after it's set. null uses the system language.
registerHuaweiPush() and others
The register functions of the phone-maker channel packages. Call them in main() before creating the client.
| Function | Package | Parameters |
|---|---|---|
registerHuaweiPush() | deeprespond_im_push_huawei | None |
registerHonorPush() | deeprespond_im_push_honor | None |
registerXiaomiPush({required String appId, required String appKey}) | deeprespond_im_push_xiaomi | AppID and AppKey from the Xiaomi open platform |
registerOppoPush({required String appKey, required String appSecret}) | deeprespond_im_push_oppo | AppKey and AppSecret from the OPPO open platform |
registerVivoPush() | deeprespond_im_push_vivo | None (AppID and AppKey go in AndroidManifest.xml) |
registerMeizuPush({required String appId, required String appKey}) | deeprespond_im_push_meizu | AppID and AppKey from the Meizu open platform |
Returns: nothing. Calling them on iOS has no effect.
drFirebaseBackgroundHandler()
Handles the IM service's call messages among FCM background data messages. Call it in the handler you registered with FirebaseMessaging.onBackgroundMessage.
| Parameter | Type | Required | Description |
|---|---|---|---|
message | RemoteMessage | Yes | The message your handler received |
Returns: Future<void>. Returns straight away for anything that isn't an IM call message.
PushPayload.parse()
Takes the IM service's im object from a push's custom data and parses it. The SDK already parses the data from notification taps, so you usually don't need this; it's useful in a notification extension or your own push handling.
| Parameter | Type | Required | Description |
|---|---|---|---|
data | Map<String, Object?> | Yes | The push's custom data, such as FCM's RemoteMessage.data or APNs's userInfo |
channel | PushChannel? | No | The channel it came from |
Returns: PushPayload?; null if it isn't an IM service push.
DRBackgroundClient.open()
Opens a background client in a background isolate. Call close() when done.
| Parameter | Type | Required | Description |
|---|---|---|---|
appKey | String | Yes (named) | The app's AppKey |
platform | DRPlatform | Yes (named) | DRFlutterPlatform.instance |
apiUrl | Uri? | No (named) | Server address; defaults to the one saved when the main isolate created its client |
log | LogOptions | No (named) | Log options |
Returns: Future<DRBackgroundClient>.
Errors: invalid_state (no_launch_info: no apiUrl was given and no client has been created on this device).
Methods of the background client:
| Method | Returns | Description |
|---|---|---|
parse(Map<String, Object?> data, [PushChannel? channel]) | PushPayload? | Same as PushPayload.parse() |
getCall(String callId) | Future<BackgroundCall?> | Looks up a call; null if it doesn't exist or the user isn't in it |
answerCall(String callId) | Future<void> | Answers an incoming call (answer only; no media connection) |
rejectCall(String callId, {String reason = 'declined'}) | Future<void> | Rejects an incoming call; reason is declined or busy |
reportBadge(int badge) | Future<void> | Reports this device's badge count |
close() | Future<void> | Closes the client |
Failures throw DRException; network errors are network_error and timeout.
Types
DRPushOptions
Push options when creating the client (DRClientOptions.push).
| Field | Type | Description |
|---|---|---|
credentials | Map<PushChannel, String> | Required. Each channel's credential name in the console; channels without one aren't registered |
voipCredential | String? | Credential name for the iOS VoIP token; defaults to the apns one. Only registered with the system call screen (DRCallOptions.useCallKit is true) |
androidChannels | List<AndroidChannel> | Android notification channels to create; empty by default |
suppressInForeground | bool | iOS: don't show the app's message, friend and group notifications in the foreground. Default true |
PushChannel
Push channels: apns, fcm, huawei, honor, xiaomi, oppo, vivo, meizu.
AndroidChannel
| Field | Type | Description |
|---|---|---|
id | String | Channel ID; must match the credential parameter in the console |
name | String | Name shown in system settings |
kind | AndroidChannelKind | Use: message for messages and notices, call for incoming calls, silent for no sound |
description | String? | Description shown in system settings |
PushPermission
Notification permission: granted, denied, notDetermined (not asked yet), or provisional (iOS provisional authorization).
PushDevice
The server's record of this device's registration.
| Field | Type | Description |
|---|---|---|
platform | String | Device platform, such as ios or android |
tokens | List<Object?> | Registered regular tokens (masked), each with its credential name, channel and so on |
voipToken | Map<String, Object?>? | iOS VoIP token (masked); null if none |
language | String? | Registered language |
raw | Map<String, Object?> | Raw data; for fields, see Push devices in the server docs |
PushPayload
The IM service's data in a push, split by kind into the classes below (a sealed class, so switch can cover every case). Every class has version (int, the structure version), kind (String) and raw (Map<String, Object?>, the raw im object). Unknown fields are ignored.
| Class | kind | Description |
|---|---|---|
MessagePush | message | New message |
RecallPush | recall | Notification replaced after a recall |
NoticePush | notice | Friend and group notices |
CallPush | call | Incoming, missed and cancelled calls |
DismissPush | dismiss | Clear the call notification (the user answered or rejected on another device) |
TestPush | test | Test push sent from the console |
UnknownPush | Other | Types added later; treat as a regular notification and just open the app on tap |
MessagePush
| Field | Type | Description |
|---|---|---|
conversationType | String | single or group |
conversationId | String | Conversation ID |
seq | int? | The message's number in the conversation, for jumping to it |
messageId | String? | Message ID |
sender | String? | The sender's username; null when the preview mode is none, for deleted users, and for messages sent as the system |
ext | Map<String, Object?>? | Custom data from push.ext when sending |
RecallPush
| Field | Type | Description |
|---|---|---|
conversationType | String | single or group |
conversationId | String | Conversation ID |
messageId | String? | ID of the recalled message |
NoticePush
When the preview mode is none, all three fields are null and a tap just opens the app.
| Field | Type | Description |
|---|---|---|
notice | String? | Notification template key, such as friend.request or group.invitation |
user | String? | The related username |
groupId | String? | The related group ID |
CallPush
| Field | Type | Description |
|---|---|---|
event | String | incoming, missed or canceled |
callId | String | Call ID |
callerName | String? | The caller's nickname, or username if there's no nickname; null when the preview mode is none |
ringExpiresAt | DateTime? | When ringing stops |
title | String? | Title carried by FCM call messages (already in the device's language) |
body | String? | The same, for the text |
data | Map<String, Object?>? | Call data: call_id, media, caller, caller_nickname, ring_expires_at and so on; null when the preview mode is none |
DismissPush
| Field | Type | Description |
|---|---|---|
callId | String | ID of the call whose notification to clear |
BackgroundCall
The result of the background client's getCall().
| Field | Type | Description |
|---|---|---|
callId | String | Call ID |
status | String | Call status: ringing, active, ended |
media | String? | audio or video |
caller | String? | The caller's username |
self | Map<String, Object?>? | The user's state in the call |
