FlutterでFCMを使ったプッシュ通知を実装してみた

FCM(Firebase Cloud Messaging)を使ってFlutterで作ったアプリでプッシュ通知を受け取れるように実装してみたのでご紹介します。
2018.08.20

大阪オフィスの山田です。新しい冷蔵庫は自動で氷を作ってくれるのですごい。今回はFCMを使ってFlutterで作ったアプリでプッシュ通知を受け取れるように実装してみたのでご紹介します。

今回やること

FCM (Firebase Cloud Messaging) でプッシュ通知の環境を作って、Flutterで作ったアプリで通知を受け取ってみます。iOSで必要になるAPNs認証キーの作り方やFirebaseでプロジェクトを作る部分については説明しません。FlutterでFCMを利用するために、ライブラリ、firebase_messagingを使用します。

開発環境

flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.26.1)
[✓] Connected devices (1 available)

flutter --version

Flutter 0.5.1 • channel beta • https://github.com/flutter/flutter.git
Framework • revision c7ea3ca377 (3 months ago) • 2018-05-29 21:07:33 +0200
Engine • revision 1ed25ca7b7
Tools • Dart 2.0.0-dev.58.0.flutter-f981f09760

準備

iOS

  • FirebaseプロジェクトにiOSアプリを追加して、GoogleService-info.plistをダウンロードします。
  • GoogleService-info.plistをプロジェクトに追加します。XcodeでRunner.xcodeprojを開いて、Runnerディレクトリの下に追加します。

  • プロジェクトの設定としてプッシュ通知を有効化します。CapabilitiesタブのPush NotificationsをONにします。

  • ios/Podfileuse_frameworks!の記述を削除します。
  • FirebaseプロジェクトにAPNs証明書、あるいはAPNs認証キーを設定します。

Android

  • FirebaseプロジェクトにAndroidアプリを追加して、google-services.jsonをダウンロードします。
  • google-services.jsonを、android/appディレクトリの直下に配置します。

以下のgradleファイルに記述を追加します。 - プロジェクトルートのbuild.gradle

    dependencies {
        classpath 'com.google.gms:google-services:4.0.0'
    }
  • アプリのbuild.gradle
dependencies {
    compile 'com.google.firebase:firebase-core:16.0.0'
}

apply plugin: 'com.google.gms.google-services'  <- ファイルの最後に追記
  • AndroidManifest.xml
<intent-filter>
    <action android:name="FLUTTER_NOTIFICATION_CLICK" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

Flutterの実装

pubspec.yamlに以下の記述を追加します。

dependencies:
  firebase_messaging: ^1.0.4

プログラムは以下のように書きました。

import 'package:firebase_messaging/firebase_messaging.dart';  // ライブラリのインポート

// 以下をStateの中に記述  
  final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
  
  @override
  void initState() {
    super.initState();
    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        print("onMessage: $message");
        _buildDialog(context, "onMessage");
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        _buildDialog(context, "onLaunch");
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        _buildDialog(context, "onResume");
      },
    );
    _firebaseMessaging.requestNotificationPermissions(
        const IosNotificationSettings(sound: true, badge: true, alert: true));
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings) {
      print("Settings registered: $settings");
    });
    _firebaseMessaging.getToken().then((String token) {
      assert(token != null);
      print("Push Messaging token: $token");
    });
    _firebaseMessaging.subscribeToTopic("/topics/all");
  }
  
  // ダイアログを表示するメソッド
  void _buildDialog(BuildContext context, String message) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return new AlertDialog(
          content: new Text("Message: $message"),
          actions: <Widget>[
            new FlatButton(
              child: const Text('CLOSE'),
              onPressed: () {
                Navigator.pop(context, false);
              },
            ),
            new FlatButton(
              child: const Text('SHOW'),
              onPressed: () {
                Navigator.pop(context, true);
              },
            ),
          ],
        );
      }
    );
  }

getTokenメソッドでFCMのトークンを取得しています。requestNotificationPermissionsでpush通知の許可を聞くようにしています。onMessage, onLaunch, onResumeメソッドはpush通知のアクションです。iOS, Android、そして通知のタイプ(notification, data messseage)、そしてアプリの状態によってどのメソッドが呼ばれるか変わります。詳細についてはこちらのページReceiving Messageを参照してください。アプリを入れている端末全体にpush通知を飛ばすために、initStateでトピック/topics/allをsubscribeしています。_buildDialogメソッドでダイアログを表示します。

実際にpush通知を出してみる。

curlでpush通知を送ってみます。your_server_keyの部分には、Firebaseのコンソール画面に移動して、設定->クラウドメッセージングのページに記載されている、サーバーキーを設定してください。

curl -H "Authorization: key=<your_server_key>"\
 -H "Content-Type: application/json"\
 -d @json/data.json\
 https://fcm.googleapis.com/fcm/send

json/data.jsonの中身

{
    "to": "/topics/all",
    "priority": "high",
    "notification" : {
        "title" : "テスト Title",
        "body" : "テスト Body"
    },
    "data" : {
        "id" : "1001",
        "message" : "message 文字列",
        "metadata" : "metadata 文字列",
        "click_action": "FLUTTER_NOTIFICATION_CLICK"
    }
}

Androidでの注意点

通知をタップして、onResume、onLaunchを発生させるには"click_action" : "FLUTTER_NOTIFICATION_CLICK"をdataの中に含める必要があります。

結果

iOS

Android

解決しなかったこと

Androidの通知の仕組み詳しくないのですが、アプリがTerminatedの時には届かなかったです。プラグインのREADME.mdには確かにterminatedの時にData messageは届かない、と記載がありますが、notificationでかつdataを含む場合でも、届かなかったです。Androidのバージョンによっても挙動が違うのかな。。。(今回使用した端末は6.0と7.0)アプリがバックグラウンド、フォアグラウンドに存在するときは正常に届きました。

最後に

余暇で楽しんでいる程度ですが、最近社内でも「Flutter触っているらしいですね」と、声をかけてくださる方が増えて嬉しいです。これからもがんばろー。