Flutter中如何使用Firebase 做消息推送(Notification)

??又到了發文的時候了,不懶散,不嬌作,寫就完了~
今天我們的主題是推送,這在所有的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推送庫到本地
圖1.2.png

二、分別設置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.1.png

2.2.2 在Xcode中,點擊Runner -> Runner -> Signing & Capabilities -> Background Modes,將Background fetch和Remote notifications勾上,如圖2.2.2


圖2.2.2.png

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

  1. 在 Firebase 控制臺中,在您的項目內依次選擇齒輪圖標、項目設置以及 Cloud Messaging 標簽頁。

  2. iOS 應用配置下的 APNs 身份驗證密鑰中,點擊上傳按鈕。如圖

    image.png

  3. 轉到您保存密鑰的位置,選擇該密鑰,然后點擊打開。添加該密鑰的 ID(可在 Apple Developer Member CenterCertificates, 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控制臺生成私鑰文件給后臺

后臺私鑰文件生成.png

七、結語

Firebase的消息推送也就講完了,其實做了好久了,現在寫寫有的還有點忘。別說,有時間是得把一些知識記錄下,也是自己重新鞏固下,也是留下一點點記錄。

申明:禁用于商業用途,如若轉載,請附帶原文鏈接。http://www.lxweimin.com/p/8b5cba526c63蟹蟹~

PS: 寫文不易,覺得沒有浪費你時間,請給個關注和點贊~ ??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容