Flutter×Firebase(CloudMessaging)で、フォアグラウンド/バックグラウンドで通知をしてみる

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認ください</b></font>
* 25万文字を超える超大作になっています!!

---

## まとめ記事
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)

# はじめに
* FIrebaseを用いた通知機能を試してみましたので、そのまとめになります。


# 概要
Firebase CloudMessaging(以下FCM)は、ユーザにプッシュ通知を送信するための環境を提供します。
バックグラウンドでも動作できるため、アプリを起動していなくても通知を受け取れるようになります。

### 注意1
https://firebase.flutter.dev/docs/messaging/usage/

公式サイトに記載がある通り、iOSシミュレータでは動作しないことが周知されています。

### 注意2
本チャプターでは、Androidのみを対象に行います。
※iOSで設定するためには、APNs認証キーの設定や権限の要求など、Androidよりもいくつか手順が多いので注意してください。


# 準備
まずは、必要なパッケージをインストールします。

```yaml:pubspec.yaml
firebase_core: ^1.1.0
firebase_messaging: ^9.1.3
flutter_local_notifications: ^5.0.0+4
```
`firebase_core`と`firebase_messaging`はFCM関連のパッケージです。`local_notification`はフォアグラウンドで受けとったメッセージを通知として処理するために用いるパッケージです。

# tokenの取得
## 実装

まず、送信先となる端末ごとのtokenを確認する必要があります。
Tokenは下記のように、非同期のgetTokenメソッドで取得することができます。

```dart:snippet.dart
_firebaseMessaging.getToken().then((String token) {
  print("$token");
});
```

処理の全体像は下記のようになります。
上記のtoken取得をinitstateで行っているだけです。

この時点ではまだメッセージの受信はできません。

```dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> main() async {
  // Firebaseの初期化用
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();

    _firebaseMessaging.getToken().then((String token) {
      print("$token");
    });

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
            child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("FCMテスト"),
          ],
        )));
  }
}
```


# 動作イメージ

```sh:result.sh
I/flutter (19699): cuKvQ621SAyZzfYgEoscnc:AP*****Tnf5Ru8lLU_*****KRdDe-4kPlzCIv*****p3UDU6Ml6A256yeRQ*****nKmH4*****wqx02ezs*****alkwT7X4QjG*****c0yM6Nfm*****YK2EhQjq*****F-0e84M8B6U
```

※適度にマスキングをしています

# フォアグラウンドでのメッセージ受信
## 実装

続いて、メッセージを受信できるように改修します。

受信は`FirebaseMessaging.onMessage.listen((RemoteMessage message) {}`によっておこなえます。
ただし、コンソールに出すだけなら、このメソッドの中で`print`をすればよいのですが、実用的ではありません。
そこで、`flutterLocalNotificationsPlugin`を使い、通知として表示されるようになります。

```dart:snippet1.dart
flutterLocalNotificationsPlugin.show(
  notification.hashCode,
  notification.title,
  notification.body,
  NotificationDetails( 
  android: AndroidNotificationDetails(
    channel.id,
    channel.name,
    channel.description,
    icon: 'launch_background',
  ),
)); 
```

この第4引数のDetailsは、特定のクラスになっているためこちらも準備します。
使い回しなどがなければ、上記に直接指定してもOKです。

```dart:snippet2.dart
const AndroidNotificationChannel channel = AndroidNotificationChannel(
  'high_importance_channel', // id
  'High Importance Notifications', // title
  'This channel is used for important notifications.', // description
  importance: Importance.high,
);
```

フォアグラウンドの処理を組み込んだ全体像は下記のようになります。

```dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';

+final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
+    FlutterLocalNotificationsPlugin();

+const AndroidNotificationChannel channel = AndroidNotificationChannel(
+  'high_importance_channel', // id
+  'High Importance Notifications', // title
+  'This channel is used for important notifications.', // description
+  importance: Importance.high,
+);

Future<void> main() async {
   // Firebaseの初期化用
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();

    _firebaseMessaging.getToken().then((String token) {
      print("$token");
    });

+    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
+      print("フォアグラウンドでメッセージを受け取りました");
+      RemoteNotification notification = message.notification;
+      AndroidNotification android = message.notification?.android;
+
+      if (notification != null && android != null) {
+        flutterLocalNotificationsPlugin.show(
+            notification.hashCode,
+            notification.title,
+            notification.body,
+            NotificationDetails(
+              android: AndroidNotificationDetails(
+                channel.id,
+                channel.name,
+                channel.description,
+                icon: 'launch_background',
+              ),
+            ));
+      }
+    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
            child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("FCMテスト"),
          ],
        )));
  }
}
```



# 動作イメージ
Cloud Messagingからテスト用メッセージの送信ができ、Androidシミュレータで受信の確認ができます。

まず、FirebaseのメニューからCloud Messagingを選択します。
![pic11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/362335a7-08e9-cf0f-8267-84a3af774591.png)

通知するタイトルとテキストを入れます。
![pic12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/054cef87-5f27-77f9-c4b8-c9cf51c2d8e6.png)

テストメッセージを送信をクリックします。
![pic13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/87763d40-570e-c0cf-4d74-a0aea2776c94.png)

先程表示させたトークンIDを入力し、テストをクリックします。
![pic14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/72da69e4-be7f-fda6-ea29-3dce92f1fc71.png)

フォアグラウンドの状態にしておくとシミュレータに通知がきます。
![pic15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/8e20c3ee-59a1-69e6-b1dc-e3f4abbab849.png)

コンソールの方にも表示されます。

```sh:result.sh
D/FLTFireMsgReceiver(21013): broadcast received for message
I/flutter (21013): フォアグラウンドでメッセージを受け取りました
```

# バックグラウンドでのメッセージ受信
## 実装


まず、`FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);`でバックグラウンド時に呼び出されるコールバックのハンドラを指定します。

続いて、`_firebaseMessagingBackgroundHandler`に呼び出されたときの処理を記述します。

これらのメソッドはトップレベルに定義する必要があります

バックグラウンドでのメッセージ受信を追加した、全体像は下記のようになります。

```dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

+// バックグラウンドの処理の実態
+// トップレベルに定義
+Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
+  await Firebase.initializeApp();
+  print("バックグラウンドでメッセージを受け取りました");
+}

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

const AndroidNotificationChannel channel = AndroidNotificationChannel(
  'high_importance_channel', // id
  'High Importance Notifications', // title
  'This channel is used for important notifications.', // description
  importance: Importance.high,
);

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

+ //バックグラウンド用
+ FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();

    _firebaseMessaging.getToken().then((String token) {
      print("$token");
    });

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print("フォアグラウンドでメッセージを受け取りました");
      RemoteNotification notification = message.notification;
      AndroidNotification android = message.notification?.android;

      if (notification != null && android != null) {
        flutterLocalNotificationsPlugin.show(
            notification.hashCode,
            notification.title,
            notification.body,
            NotificationDetails(
              android: AndroidNotificationDetails(
                channel.id,
                channel.name,
                channel.description,
                icon: 'launch_background',
              ),
            ));
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
            child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("FCMテスト"),
          ],
        )));
  }
}
```



# 動作イメージ

フォアグラウンドと同じように、Firebaseからメッセージを送信します。
※フォアグラウンドじとの違いがわかるように、タイトルとメッセージを変えてあります。

![pic16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/eabdc533-2024-4986-26fc-b4998b787c53.png)

送信の仕方は変わりません。
トークンを指定してテストをクリックします。

![pic17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/e14cf67b-c1c2-ef87-03a0-688dedb898b3.png)

アプリ起動中でなくても、通知が来ていることが分かります。

![pic18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/9de20871-1ed8-905b-26ac-1c5a80d760cf.png)

コンソールの方にも表示されます。

```sh:result.sh
I/FLTFireMsgService(19699): FlutterFirebaseMessagingBackgroundService started!
W/FLTFireMsgService(19699): Attempted to start a duplicate background isolate. Returning...
D/FLTFireMsgReceiver(19699): broadcast received for message
W/FirebaseMessaging(19699): Unable to log event: analytics library is missing
W/FirebaseMessaging(19699): Missing Default Notification Channel metadata in AndroidManifest. Default value will be used.
I/flutter (19699): バックグラウンドでメッセージを受け取りました
```


アプリが終了していてもバックグラウンドプロセスとして残っていれば、通知は届きます