【Flutter】Firebase MessaigingでPush通知を実装する

# 背景
FlutterでFirestoreのPush通知を実装する機会があり、大変複雑だったため備忘録として記録します。

# 目標
iOS、Androidで以下の状態で動作することを目標に設定しました。
・フォアグラウンド、バックグラウンド、アプリをキルした状態で通知が受け取れる
・通知受け取り時に通知バナーが表示される


# 事前設定
事前に以下の設定を行っておきます。（iOS向け）
・Apple Developerのidentifier>CapabilityにPush Notificationを追加
・Apple DeveloperからKeyファイルを作成、ダウンロード（.p8ファイル）
・Firebaseのプロジェクトの設定＞Cloud Messaging＞Apple アプリの構成 > APNs 認証キー へKeyファイルをアップロード


# パッケージのインストール

pubspec.yamlに使用するパッケージを入力し、保存します。
```
dependencies:
    firebase_messaging: ^15.0.1
    flutter_local_notifications: ^17.1.2
```
※flutter_local_notificationsはAndroidのフォアグラウンド通知で使用します。

ターミナルで下記のコマンドを実行します。
```
flutter pub get
```

# iOSの設定

### XCode

Runner > Target Runner > Signing & Capabilities で＋Capabilityボタンから、以下を追加します。
・Push Notifications
・Background Modes

Background Modesは、以下にチェックを入れてください。
・Background fetch 
・Remote notifications 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3772194/23092ef3-e418-e8cb-7bbb-1b632cdb5860.png)


# Androidの設定

・Manifest.xmlの設定は以下の通りです。
```
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 追加 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> 
    <!-- 追加 -->
```

# iOS、Androidバックグラウンド 実装 

### main.dart

```
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Firebase初期化処理
  await Firebase.initializeApp();

  // バックグラウンド通知の設定
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(
    const ProviderScope(
      child: App(),
    ),
  );
}
```

### Push通知を管理するクラス
※AppPushMessageは独自で定義したPush通知のメッセージを受け取るモデルです。FCMとLocalNotificationで受け取る型が違うので、AppPushMessageに変換して使用しています。
```
class PushNotificationRepository {
  /// FCM Tokenの取得
  Future<String?> getFcmToken() async {
    final fcmToken = await FirebaseMessaging.instance.getToken();
    return fcmToken;
  }

  /// Push通知の許可設定
  Future<void> requestPermission() async {
    // フォアグラウンドで通知が表示されるオプションの設定
    await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );

    await FirebaseMessaging.instance.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
  }  
  
  /// Push通知初期設定
  Future<void> initializePushNotification({
  　// Push通知タップ後の処理
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    // 通知許可のダイアログを表示
    requestPermission();
  }

  /// バックグラウンド
  Future<void> onMessageOpenedApp({
  　// Push通知タップ後の処理
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      final appPushMessage = _convertRemoteMessage(message);

      onTapMessage(appPushMessage);
    });
  }

  /// アプリが起動していない状態
  Future<void> onTerminatedMessage({
    // Push通知タップ後の処理
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    final remoteMessage = await FirebaseMessaging.instance.getInitialMessage();

    if (remoteMessage != null) {
      final appMessage = _convertRemoteMessage(remoteMessage);
      onTapMessage(appMessage);
    }
  }
}

AppPushMessage _convertRemoteMessage(RemoteMessage message) {
  return AppPushMessage(
    hash: message.hashCode,
    title: message.notification?.title ?? '',
    message: message.notification?.body ?? '',
  );
}

AppPushMessage _convertNotificationResponse(
    NotificationResponse response) {
  return AppPushMessage(
    hash: response.id ?? 0,
  );
}
  ```

### Pusu通知の初期化設定
```
  Future<void> initializePushNotification(
      {Function(AppPushMessage)? onTapMessage}) async {

    // FCMトークン取得
    await pushNotificationRepository.getFcmToken();

    // iOS,Androidバックグラウンド設定   
    pushNotificationRepository.onMessage(onTapMessage: dispatchOnMessage);
   
    // アプリがキルされている状態設定   
    pushNotificationRepository.onTerminatedMessage(
      onTapMessage: onTapMessage,
    );
  }
}
```


# Androidのフォアグラウンド通知
Androidでフォアグラウンド通知を受信するには、別途設定が必要です。

### ローカル通知を管理するクラスを追加
```
class LocalNotificationManager {

  AndroidNotificationChannel androidNotificationChannel =
      const AndroidNotificationChannel(
    'channel_id',
    'channel_name',
    importance: Importance.max,
  );

  /// ローカル通知の初期化設定
  Future<void> initialLocalNotification({
    required AppPushMessage Function(NotificationResponse)
        convertToAppPushMessage,
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    const initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_notification');
    const initializationSettingsIos = DarwinInitializationSettings();

    const initializationSettings = InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIos,
    );

    await FlutterLocalNotificationsPlugin().initialize(
      initializationSettings,
      onDidReceiveBackgroundNotificationResponse: (message) {
        final appPushMessage = convertToAppPushMessage(message);
        onTapMessage(appPushMessage);
      },
    );

    await FlutterLocalNotificationsPlugin()
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(androidNotificationChannel);
  }

  /// 通知を表示
  Future<void> showPushNotification(AppPushMessage appPushMessage) async {
    await FlutterLocalNotificationsPlugin().show(
      appPushMessage.hash,　// プッシュ通知のハッシュ値
      appPushMessage.title,　// プッシュ通知のタイトル
      appPushMessage.message,　// プッシュ通知のメッセージ
      NotificationDetails(
        android: AndroidNotificationDetails(
          androidNotificationChannel.id,
          androidNotificationChannel.name,
          icon: '@mipmap/ic_notification',
        ),
      ),
    );
  }
}

```

### プッシュ通知を管理するクラスにLocalNotificationManagerの初期化処理と、フォアグラウンド通知を受け取る処理を追加

```
class PushNotificationRepository {
  ...
  
  final FlutterLocalNotificationsPlugin localNotificationManager;
  /// Push通知初期設定
  Future<void> initializePushNotification({
  　// Push通知タップ後の処理
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    // 通知許可のダイアログを表示
    requestPermission();

    // ローカル通知の初期化　←追加
    localNotificationManager.initialLocalNotification(
      // convertToAppPushMessageにLocalNotificationで受け取ったRemoteMessageをAppPushMessage（独自のモデル）に変換する処理
      convertToAppPushMessage: _convertNotificationResponse,
      onTapMessage: onTapMessage,
    );
    
  }
  
  ...

  /// Androidのフォアグラウンド通知
  Future<void> onMessage({
  　// Push通知タップ後の処理
    required void Function(AppPushMessage) onTapMessage,
  }) async {
    FirebaseMessaging.onMessage.listen((message) async {
      final appPushMessage = _convertRemoteMessage(message);
      AndroidNotification? android = message.notification?.android;

      if (android != null) {
        await localNotificationManager.showPushNotification(appPushMessage);
      }
    });
  }
  
  ...

}
```
プッシュ通知の初期化処理にAndroidフォアグラウンド設定を追加する。
```
  Future<void> initializePushNotification(
      {Function(AppPushMessage)? onTapMessage}) async {

    // FCMトークン取得
    await pushNotificationRepository.getFcmToken();

    // Androidフォアグラウンド設定    ←追加
    pushNotificationRepository.initializePushNotification(
      onTapMessage: onTapMessage!,
    );
    
    // iOS,Androidバックグラウンド設定   
    pushNotificationRepository.onMessage(onTapMessage: dispatchOnMessage);
    
    // Androidフォアグラウンド設定     ←追加
    pushNotificationRepository.onMessageOpenedApp(onTapMessage: (message) {
      onTapMessage(message);
    });
    
    // アプリがキルされている状態設定   
    pushNotificationRepository.onTerminatedMessage(
      onTapMessage: onTapMessage,
    );
  }
}
```


# 呼び出し

作成したinitializePushNotificationの初期化処理をApp.dartなどで呼び出すと、Push通知のタップ後の処理も設定することができます。
（クラスは省略）
```
initializePushNotification(onTapMessage: (message) {
　// Push通知タップ後に実行したい処理を渡す
  print('プッシュ通知が呼ばれたよ');
});
```

# Android：バックグラウンド状態で通知バナーを表示する様にする
そのままの設定ではアプリキルの状態で通知を受け取った際にAndroidでは通知バナーが表示されないので設定を追加します。

### AndroidのManifest.jsonの設定
```
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />　←通知アイコン

<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id"/>　←デフォルトの通知IDを定義
```

### build.gradle
Manifest.jsonで定義したdefault_notification_channel_idにlocalNotificationで設定したチャネルIDを設定します。
以下の設定で、バックグラウンド状態でも通知バナーが表示されるようになります。
```
    defaultConfig {
        ...
        // localNotificationで設定したチャネルIDを設定する
        resValue "string", "default_notification_channel_id", "channel_id"
        ...   
    }
```


# 参考記事
[Flutter で Firebase Cloud Messaging クライアント アプリを設定する](https://firebase.google.com/docs/cloud-messaging/flutter/client?hl=ja)

[Flutter × FCMでプッシュ通知を実装する](https://zenn.dev/flutteruniv_dev/articles/flutter_push_notification)


[Firebase Notifications & Flutter Local Notifications](https://medium.com/@1998.singh.amarjit/firebase-notifications-flutter-local-notifications-a7d894dc60fe)


# 告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。

https://eda-inc.jp/recruit/

みなさまからのご応募をお待ちしております。