??又到了發文的時候了,不懶散,不嬌作,寫就完了~
今天我們的主題是推送,這在所有的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捕獲
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="*******">
<application
....>
<!-- 推送通知圖標-->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification_icon" />
<activity
android:name=".MainActivity"
...>
...
<!-- 通知點擊intentfilter -->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
...
</application>
</manifest>
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文件中
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
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<UNUserNotificationCenterDelegate>) 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 身份驗證密鑰中,點擊上傳按鈕。如圖
image.png -
轉到您保存密鑰的位置,選擇該密鑰,然后點擊打開。添加該密鑰的 ID(可在 Apple Developer Member Center 的 Certificates, Identifiers & Profiles 中找到),然后點擊上傳。
image.png
三、推送接受消息回調方法實現
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<String, dynamic> 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<void> backgroundMessageHandler(Map<String, dynamic> 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<void> 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<void> 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的消息推送也就講完了,其實做了好久了,現在寫寫有的還有點忘。別說,有時間是得把一些知識記錄下,也是自己重新鞏固下,也是留下一點點記錄。
申明:禁用于商業用途,如若轉載,請附帶原文鏈接。http://www.lxweimin.com/p/8b5cba526c63蟹蟹~
PS: 寫文不易,覺得沒有浪費你時間,請給個關注和點贊~ ??