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的消息推送也就讲完了,其实做了好久了,现在写写有的还有点忘。别说,有时间是得把一些知识记录下,也是自己重新巩固下,也是留下一点点记录。

申明:禁用于商业用途,如若转载,请附带原文链接。https://www.jianshu.com/p/8b5cba526c63蟹蟹~

PS: 写文不易,觉得没有浪费你时间,请给个关注和点赞~ 😁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容