又到了发文的时候了,不懒散,不娇作,写就完了~
今天我们的主题是推送,这在所有的App中都是最基本的功能,第三方做推送的平台挺多的,这里就不一一列举了,我们主要是介绍如何使用Firebase Clound Messaging功能做推送。
老规矩,先上酸菜~
《Flutter的拨云见日》系列文章如下:
1、Flutter中指定字体(全局或者局部,自有字库或第三方)
2、Flutter发布Package(Pub.dev或私有Pub仓库)
3、Flutter中解决输入框(TextField)被键盘遮挡问题
4、Flutter 如何在不同环境上运行和打包(多环境部署)
5、Flutter 中为Firebase提供多个构建环境分离配置
6、Flutter中Firebase实时数据库Database使用
7、Flutter中如何使用Firebase 做消息推送(Notification)
一、引入firebase_messaging库
因为我们前面几篇写了关于Firebase的工程建立、项目创建和接入、多环境分离部署等,这也是为我们这篇推送做一些前期的准备工作。这里就不细讲了,请参考前文~
1.1 首先,我们需要引入pub.dev上的firebase_messaging第三方库
在pubspec.yaml文件中加入
dependencies:
flutter:
sdk: flutter
firebase_messaging: 7.0.3
1.2 使用flutter pub get或者在Android Studio中使用图形化操作(如图1.2),下载firebase推送库到本地
二、分别设置Android和Ios工程推送配置
2.1 Android端配置
2.1.1 首先,我们前一篇文章讲了如何分离Firebase环境,讲了如何配置不同环境的google-service.json,当然如果你不分离环境,只是先玩一玩也可以直接将该文件放在android/app目录下。
2.1.2 然后我们需要在根build.gradle文件中添加google-services依赖,这都是老生常谈了,不多讲了~
dependencies {
classpath 'com.google.gms:google-services:4.3.3'
}
2.1.3 在app/build.gradle文件中添加插件和依赖(PS: 熟悉Android都应该知道有两个build.gradle文件)
apply plugin: 'com.google.gms.google-services'
dependencies {
implementation 'com.google.firebase:firebase-messaging:20.2.4'
}
2.1.4 在app/src/main/AndroidManifest.xml文件中添加如下intentfilter,这是为了消息通知被点击时,被firebase-message捕获
...
...
2.1.5 如果android工程下没有Application.java文件,新建一个(android/app/main/kotlin)/**Application.kt),并且记得修改AndroidMainfest.xml中application的名字与其一致
class ***Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback{
override fun onCreate() {
super.onCreate()
FlutterFirebaseMessagingService.setPluginRegistrant(this)
}
override fun registerWith(registry: PluginRegistry?) {
FirebaseMessagingPlugin.registerWith(registry?.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"))
}
}
2.2 iOS端配置
2.2.1 首先,也是firebase ios配置文件GoogleService-Info.plist的引入,可以参照如何分离Firebase环境,如不分离环境也可以直接放置在Runner下如图2.2.1
2.2.2 在Xcode中,点击Runner -> Runner -> Signing & Capabilities -> Background Modes,将Background fetch和Remote notifications勾上,如图2.2.2
2.2.3 如果您需要禁用FCM iOS SDK完成的方法转换(以便可以将此插件与其他Notificatio plugin一起使用),则将以下内容添加到应用程序的Info.plist文件中
FirebaseAppDelegateProxyEnabled
2.2.4 在AppDelegete.m或AppDelegete.swift中加入以下代码
Swift:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
...
}
}
Objective-C:
if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate = (id) self;
}
2.2.5 在Apple Store开发者账号中获取APNs令牌
我们需要在Apple Store, 配置FCM APNS(https://firebase.google.com/docs/cloud-messaging/ios/certs)
这里分为两个部分:创建身份验证密钥和创建应用ID。因为文章篇幅问题,而且官网写的比较详细,请大家自行参考官网链接,已附上。
2.2.6 最后需要配置将APNs令牌映射到FCM注册令牌
接下来,将你刚刚创建好的的 APNs 身份验证密钥上传到 Firebase。如果您还没有 APNs 身份验证密钥,请参阅配置 FCM APNs。
在 Firebase 控制台中,在您的项目内依次选择齿轮图标、项目设置以及 Cloud Messaging 标签页。
-
在 iOS 应用配置下的 APNs 身份验证密钥中,点击上传按钮。如图
-
转到您保存密钥的位置,选择该密钥,然后点击打开。添加该密钥的 ID(可在 Apple Developer Member Center 的 Certificates, Identifiers & Profiles 中找到),然后点击上传。
三、推送接受消息回调方法实现
3.1 介绍一下firebase_messaging,推送几个回调方法触发时机
App在前台时 | App在后台时 | App进程被干掉时 | |
---|---|---|---|
Notification on Android | onMessage | Notification被传递到系统,当用户点击推送通知时,如果设置了click_action: FLUTTER_NOTIFICATION_CLICK, 则onResume被触发 | Notification被传递到系统,当用户点击推送通知时,如果设置了click_action: FLUTTER_NOTIFICATION_CLICK, 则onLaunch被触发。 |
Notification on iOS | onMessage | Notification被传递到系统,当用户点击推送通知时, 则onResume被触发 | Notification被传递到系统,当用户点击推送通知时,则onLaunch被触发 |
Data Msg on Android | onMessage | onMessage | 插件不支持,消息丢失 |
Data Msg on iOS | onMessage | 消息由FCM存储,并在应用回到前台时通过onMessage触发 | 消息由FCM存储,并在应用回到前台时通过onMessage触发 |
因为Firebase 推送消息有两种一种是Notification ,一种是Data message消息,以上表格是两种消息分别在Android、iOS平台应用状态不同时的回调接口情况
Flutter 处理代码如下:
static void handleNotification() {
if(firebaseMessaging == null){
firebaseMessaging = FirebaseMessaging();
}
firebaseMessaging.configure(
//处理前台app接受消息,可以在使用flutter_local_notifications插件再发出一个本地通知
onMessage: (message) => handleMessage(message),
//处理从系统通知栏点击推送时的页面跳转问题
onLaunch: (message) => startToRedirectByNotification(message, source: 'onLaunch'),
onResume: (message) => startToRedirectByNotification(message, source: 'onResume'),
onBackgroundMessage: backgroundMessageHandler //todo
);
}
static handleMessage(Map message) {
try {
String notificationPayload = '';
String notificationTitle = '';
String notificationContent = '';
if (Platform.isAndroid) {
notificationPayload = json.encode(message['data']);
notificationTitle = message['notification']['title'];
notificationContent = message['notification']['body'];
} else {
notificationPayload = json.encode(message);
notificationTitle = message['aps']['alert']['title'];
notificationContent = message['aps']['alert']['body'];
}
sendLocalNotification(notificationTitle, notificationContent, notificationPayload);
}catch(error){
LogUtil.e(error);
}
}
static Future backgroundMessageHandler(Map message) {
// to do
}
3.2 firebase_messaging库如何本地解绑
基于因为Login之后,token会有过期的行为。当token过期后(401) ,一般App会退出登录重定向到登录界面,这是一般要解绑推送,不然都退出登录了还能收到推送,这看似不太合适。
如果是正常Sign Out流程话,我们会调用后台的unbind接口和服务端解绑,这样就收不到推送了。
但是这种就不适用token过期的情况了,token过期后,后台和服务器解绑的unbind接口已经调不通了,这时就尴尬了,不可能退出登录还在收推送吧?
这样就需要使用firebase_messaging库进行本地解绑,使本地推送库不处理服务端发来的推送。
//这里调用这个方法就可以了,删除绑定的token
/// Resets Instance ID and revokes all tokens. In iOS, it also unregisters from remote notifications.
///
/// A new Instance ID is generated asynchronously if Firebase Cloud Messaging auto-init is enabled.
///
/// returns true if the operations executed successfully and false if an error ocurred
FirebaseMessaging().deleteInstanceID();
四、本地推送库引入(flutter_local_notifications)
4.1 本地推送库发送本地Notification
根据三,我们在处理Firebase message onMessage消息时,根据项目需要也需要是一个推送,从推送时机表格中,我们看到如果app在前台,收到firebase推送消息时,是不会产生一个系统消息通知出来,所以这里需要自己本地发出一个Notification.
这里我们使用到的是pub.dev上的flutter_local_notifications插件解决,具体用法就自己上去看一看,我这边配置大概如下,使用大同小异。
class LocalNotificationServer {
static FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
static initLocalNotification(){
var initializationSettingsAndroid = AndroidInitializationSettings('ic_notification_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: onDidReceiveLocalNotification);
var initializationSettings = new InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
}
static Future onDidReceiveLocalNotification(
int id, String title, String body, String payload) async {
...
}
static Future onSelectNotification(String payload) async {
...
}
static Future showNotification({int id, String title, String content, String payload}) async {
//安卓的通知配置,必填参数是渠道id, 名称, 和描述, 可选填通知的图标,重要度等等。
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'your channel id',
'your channel name',
'your channel description',
importance: Importance.max,
priority: Priority.high,
styleInformation: BigTextStyleInformation('')
);
//IOS的通知配置
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics);
//显示通知,其中 0 代表通知的 id,用于区分通知。
await flutterLocalNotificationsPlugin.show(
id, title, content, platformChannelSpecifics,
payload: payload);
}
}
4.2 本地推送库退出登录时删除显示在状态栏的推送通知
当我们退出登录时,我们有需求清理本App显示在状态栏的的通知,不然都退出登录了,点击推送通知还能跳到应用内部页面,不合适!!!
可以使用下面的方法
/// Cancel/remove the notification with the specified id.
///
/// This applies to notifications that have been scheduled and those that
/// have already been presented.
Future cancel(int id) async {
await FlutterLocalNotificationsPlatform.instance?.cancel(id);
}
/// Cancels/removes all notifications.
///
/// This applies to notifications that have been scheduled and those that
/// have already been presented.
Future cancelAll() async {
await FlutterLocalNotificationsPlatform.instance?.cancelAll();
}
五、firebase_messaging和flutter_local_notifications存在的问题和解决
我在使用firebase_messaging和flutter_local_notifications两个库做功能测试时,发现绝大部分情况都是非常正常,我测着测着就发现了问题,着两个库都存在在某些情况下都调用多次处理推送的回调方法,如firebase_messaging的onLaunch方法和flutter_local_notifications的onSelectNotification方法。
5.1 firebase_messaging库问题
这个库在我使用的时候是7.0.3版本,在Android机型上,将app置于后台时,发出推送通过系统通知点击后会触发多次onLaunch。这个问题当时在github issue上有提过和解决方案,当时flutter官方没有采纳,我等不及,就自己copy了一份自己修改上传到本地pub.dev上了。
但是在实际测试使用推送过程中,我发现其实这问题在ios上也有,也是需要修改ios端本地代码的。
我的修改是基于firebase_messaging 7.0.3上修改的,最近看了下好像更新了,不一样的写法了,可能官方已经解决了这个问题,这里还是记录下,给与参考。
5.1.1 Android端的代码修改
Android端出现问题呢,原因在于:当App在后台进程被杀时,插件接受到推送会直接通过系统通知形式展示,当你点击Notification时,通过调用OnLaunch 来启动应用并通过你的逻辑完成相应功能,这并没啥问题,当你再次返回桌面,也就是将应用置于后台时,再进入应用,会启动onLaunch方法,将你的消息通知再来走一边。
修改文件android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java
@Override
public void onMethodCall(final MethodCall call, final Result result) {
if ("FcmDartService#start".equals(call.method)) {
...
} else if ("FcmDartService#initialized".equals(call.method)) {
...
} else if ("configure".equals(call.method)) {
...
//我主要修改了下这里,增加了intent的Flag校正,是从History来的不回调OnLaunch方法
if (mainActivity != null && !launchedActivityFromHistory(mainActivity.getIntent())) {
sendMessageFromIntent("onLaunch", mainActivity.getIntent());
}
result.success(null);
} else if ("subscribeToTopic".equals(call.method)) {
...
}
}
private static boolean launchedActivityFromHistory(Intent intent) {
return intent != null && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY;
}
5.1.2 IOS端的代码修改
IOS端消息通知出现两次原因呢:场景和Android一致,这里不多说,原因在于:首先消息通知先在onLaunch方法中消费一次,置于后台后,再次进入应用,本身的IOS系统的消息中心缓存的通知信息又会再次触发了一次,所以这里我在configure方法通道中将其屏蔽了
修改文件:ios/Classes/FLTFirebaseMessagingPlugin.m
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *method = call.method;
if ([@"requestNotificationPermissions" isEqualToString:method]) {
...
} else if ([@"configure" isEqualToString:method]) {
[FIRMessaging messaging].shouldEstablishDirectChannel = true;
[[UIApplication sharedApplication] registerForRemoteNotifications];
//modify by ** 20201015 for firebase Notification display twice
// if (_launchNotification != nil && _launchNotification[kGCMMessageIDKey]) {
// [_channel invokeMethod:@"onLaunch" arguments:_launchNotification];
// }
result(nil);
} else if ([@"subscribeToTopic" isEqualToString:method]) {
...
} else if ([@"unsubscribeFromTopic" isEqualToString:method]) {
...
}
}
5.2 flutter_local_notifications库问题
这个库了我使用的是2.0.0版本,该库是用于发送本地消息通知用的,这个库到是Android端没啥问题,不出出现两次的情况,因为解决firebase_messaging Android端消息重复问题是从这里得到的灵感。而后来,我发现了firebase_messaging在IOS端也有消息消费两次的问题,我就考虑了这个库IOS端是不是也有这问题,果不其然,确实有。
PS: 问题发生场景和firebase_messaging一致,不重复说了。
这里就解决IOS端重复的问题:
位置:ios/Classes/FlutterLocalNotificationsPlugin.m
- (void)requestPermissionsImpl:(bool)soundPermission
alertPermission:(bool)alertPermission
badgePermission:(bool)badgePermission
checkLaunchNotification:(bool)checkLaunchNotification result:(FlutterResult _Nonnull)result{
if(@available(iOS 10.0, *)) {
...
if(checkLaunchNotification && self->_launchPayload != nil) {
[self handleSelectNotification:self->_launchPayload];
self->_launchPayload = nil; //modify by ** 20201015 for LocalNotification display twice
}
...
}];
} else {
...
if(checkLaunchNotification && _launchNotification != nil && [self isAFlutterLocalNotification:_launchNotification.userInfo]) {
NSString *payload = _launchNotification.userInfo[PAYLOAD];
//modify by ** 20201015 for LocalNotification display twice
if(payload && payload != NULL && ![payload isEqualToString:@""]){
[self handleSelectNotification:payload];
}
_launchNotification.userInfo = nil;
}
result(@YES);
}
}
至于原因吗,也和firebase_messaging IOS端一致
六、后台初始化SDK, 需要在Firebase控制台生成私钥文件给后台
七、结语
Firebase的消息推送也就讲完了,其实做了好久了,现在写写有的还有点忘。别说,有时间是得把一些知识记录下,也是自己重新巩固下,也是留下一点点记录。
申明:禁用于商业用途,如若转载,请附带原文链接。https://www.jianshu.com/p/8b5cba526c63蟹蟹~
PS: 写文不易,觉得没有浪费你时间,请给个关注和点赞~