見出し画像

FlutterとFirebase Cloud Messagingでプッシュ通知を実装する その1 〜Android編〜



はじめに


みなさんお元気ですか。
SHIFT DAAE(ダーエ)グループ所属のsakuraiです。

Flutterを利用した開発で、バックエンドの特定の処理のタイミングでプッシュ通知を送信する要件に対応するため、Firebase Cloud Messagingを利用しました。

1からセットアップするとなかなかの長い道のりになりましたので、実施した内容をブログにまとめようと思います。
これからFlutterにプッシュ通知を導入する方の参考になれば幸いです。

概要


今回のブログではタイトルの通り、Flutterを利用したアプリにFirebaseを導入しFirebase Cloud Messaging(以下FCM)
を利用してプッシュ通知を導入します。

Firebaseの導入、AndroidとiOSのプッシュ通知設定の理解が必要になるため
初めて対応する場合は、どこに何の作業が必要になるかを把握することに時間がかかると思います。
ですので、まずは作業全体を一覧化しました。

下記作業を一通り行うことでプッシュ通知を導入、動作確認まで行うことが可能です。

  • Firebaseの設定

    • Android、iOS共通作業

      • Firebaseプロジェクトの作成

      • プッシュ通知用のアプリの追加(Android,iOSそれぞれに1つ)

    • iOS向けの設定

      • APNキーの発行、プロビジョニングファイル修正などの対応

      • Firebase設定の更新

  • Flutterの設定

    • Android、iOS共通作業

      • Flutterパッケージの追加

    • Android向けの設定

      • Firebase、FCM初期化設定

      • プッシュ通知設定

      • ローカル通知の設定

    • iOS向けの設定

      • Firebase、FCM初期化設定

      • プッシュ通知設定

  • ローカルからのプッシュ通知送信テスト

    • Firebase Admin SDKを利用したPush通知送信テスト環境作成

今回のブログの対象範囲

今回は上記のFirebaseの設定とAndroid向けの設定と動作確認を対象といたします。 iOS向けの設定について次回記載予定です。

また実際にプッシュ通知を利用する場合は
バックエンドサーバーの処理結果からプッシュ通知を送信する等の構成になることが多いと思いますが
今回はこの部分はセットアップの対象外とし、
FirebaseとFlutterアプリ間での通知の仕組みのみを対象
とさせていただきます。

導入前提と導入環境


導入前提として、以下のセットアップが完了している状態とします。

  • Flutter SDK インストール済みで新規のFlutterプロジェクトが作成できる。

  • Firebaseのプロジェクトが作成済み。

  • Firebase CLI インストール済み。

  • Apple Developer Programに登録していて、デベロッパーアカウントがある。

  • 作業端末でnode.jsが利用できる。(プッシュ通知の動作確認用)

Flutterのバージョンは3.16.0としています。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.16.0, on macOS 13.6 22G120 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 15.0)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] VS Code (version 1.84.2)
[✓] Connected device (3 available)
[✓] Network resources

それでは以下で実際の手順を記載していきます。

プッシュ通知確認用Flutterプロジェクトの作成


flutter createコマンドでプロジェクトを作成します。

% cd ~ // ホームにFlutterプロジェクトを作成
% flutter create --platforms ios,android -t app --org com.example.sakurai --project-name push_notification_sample ./push_notification_sample  // IDは適宜変更してください

Firebaseプロジェクトにアプリを追加する


Firebaseプロジェクトに新規追加したFlutterプロジェクト用のアプリを追加します。
FlutterFire CLI を利用すると1コマンドでプロジェクトの作成とFlutterの設定を行えます。

% dart pub global activate flutterfire_cli
// プロジェクトID、とアプリケーションIDは適宜変更してください
% flutterfire configure --platforms=ios,android --project=<プロジェクトID> --android-package-name=com.example.sakurai.push_notification_sample  --ios-bundle-id=com.example.sakurai.pushNotificationSample

上記コマンドでFirebaseプロジェクト上にAndroidとiOSのアプリが作成されます。
また、下記のファイルがFlutterプロジェクト内に作成されます。
念のため、.gitignoreに追加してGitHubなどのリポジトリの管理対象外としています。

# .gitignore
google-services.json
GoogleService-Info.plist
firebase_app_id_file.json
firebase_options.dart

Flutterパッケージの追加


Firebase Cloud Messagingを利用するため、
firebase_corefirebase_messaging パッケージを追加します。

% flutter pub add firebase_core
% flutter pub add firebase_messaging

Firebase、FCM初期化設定(Android)


次にAndroidアプリの設定を行います。 まずはビルド時に必要な設定を./android/build.gradleに追記します。

# ./android/build.gradle
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.10'        
    }

次にFlutterのでFirebaseの初期化設定とFCMの設定を追加します。
mainメソッドに以下のように追記を行います。

# ./main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  final messagingInstance = FirebaseMessaging.instance;
  messagingInstance.requestPermission();

  final fcmToken = await messagingInstance.getToken();
  debugPrint('FCM TOKEN: $fcmToken');

  runApp(const MyApp());
}

ここまでの作業を行うことで、Androidアプリと Firebaseの接続が完了し、プッシュ通知を受け取ることができます。
アプリを起動すると、以下のようにFCMで利用するトークンが発行されるようになります。

次にこのトークンを利用して動作確認を行います。

プッシュ通知の動作確認


まだ設定は続きますが、ここでひとまず、プッシュ通知の動作確認を行います。
Firebase Admin SDKを利用してローカル端末からプッシュ通知の動作確認を行うため、node.jsのプロジェクトをFlutterプロジェクト外の適切な場所に作成します。

% cd ~ // ホームに動作確認用のプロジェクトを作成
% mkdir fcm-test
% npm init
% npm install firebase-admin
% touch fcmTest.js

次にFirebaseプロジェクトの設定からサービスアカウントタブを選択し、
新しい秘密鍵を作成しJSONファイルをダウンロードします。

ダウンロードしたファイルをserviceAccountKey.jsonとリネームして、fcm-testプロジェクトへ格納
続いて、fcmTest.js通知送信用の処理を記載します。
registrationTokenには、Flutterのログに出力されたトークンを設定してください。

# ./fcmTest.js
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json'); 

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

const registrationToken = 'FCMトークン';  

const message = {
  notification: {
    title: 'テスト通知',
    body: 'テストのプッシュ通知です'
  },
  data: {
    testData: '通知に含めたいデータなど',
  },
  token: registrationToken,
};

admin.messaging().send(message)
  .then((response) => {
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });

以下のコマンドを実行し、プッシュ通知の動作確認ができます。

% node fcmTest.js

ローカル通知の設定


ここまでで、プッシュ通知を受け取ることができるようになりましたが、
引き続き対応が必要です。

プッシュ通知を受け取る状態としてアプリケーションでは以下の3つの状態が考えられます。

  • フォアグラウンド

    • アプリが起動していて、画面に表示されている状態

  • バックグラウンド

    • アプリが起動しているがホーム画面や他のアプリを利用している状態

  • アプリ停止

    • フォアグランドでもバックグラウンドでもアプリが起動していない状態

FlutterとFCMを利用した通知でデフォルトの設定の場合、アプリ停止状態や、バックグラウンド状態の場合は通知が画面上部のステータスバーに表示されますが、フォアグラウンドでは何も表示されません

また、通知の表示については重要度を設定しないとヘッドアップ通知(プッシュ通知時に画面上部に通知がしばらく表示される)
が表示されません

これらの対応するためには受け取ったプッシュ通知をトリガーにローカル通知として表示を行う必要があります

参考:

私のPJではアプリがフォアグランドの場合でも他の状態と同じような通知表示したい、またヘッドアップ通知についても表示したいという要件があったため追加対応が必要となりました。

ですので、これら対応の手順についても記載いたします。
まずはローカル通知用のパッケージであるflutter_local_notificationsを追加します。

% flutter pub add flutter_local_notifications

次にローカル通知を行うための設定をAndroidManifest.xmlに追記します。

# ./android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="push_notification_sample"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <!-- FLUTTER_NOTIFICATION_CLICKを追記 -->
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <!-- default_notification_channel_idを追記 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="default_notification_channel" />  
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- 権限設定を追記 -->    
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

最後にmain.dartに各状態の通知が来た場合の処理と通知の重要度の設定を行います。

# ./main.dart

import 'dart:convert';
import 'dart:io';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:push_notification_sample/firebase_options.dart';

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

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  final messagingInstance = FirebaseMessaging.instance;

  final fcmToken = await messagingInstance.getToken();
  debugPrint('FCM TOKEN: $fcmToken');

  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
  if (Platform.isAndroid) {
    final androidImplementation =
        flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>();
    await androidImplementation?.createNotificationChannel(
      const AndroidNotificationChannel(
        'default_notification_channel',
        'プッシュ通知のチャンネル名',
        importance: Importance.max,
      ),
    );
    await androidImplementation?.requestNotificationsPermission();
  }

  // 通知設定の初期化を行う
  _initNotification();

  // アプリ停止時に通知をタップした場合はgetInitialMessageでメッセージデータを取得できる
  final message = await FirebaseMessaging.instance.getInitialMessage();
  // 取得したmessageを利用した処理などを記載する

  runApp(const MyApp());
}

Future<void> _initNotification() async {
  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    // バックグラウンド起動中に通知をタップした場合の処理
  });

  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    final notification = message.notification;
    final android = message.notification?.android;

    // フォアグラウンド起動中に通知が来た場合の処理

    // フォアグラウンド起動中に通知が来た場合、
    // Androidは通知が表示されないため、ローカル通知として表示する
    // https://firebase.flutter.dev/docs/messaging/notifications#application-in-foreground
    if (Platform.isAndroid) {
      // プッシュ通知をローカルから表示する
      await FlutterLocalNotificationsPlugin().show(
        0,
        notification!.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            'default_notification_channel',
            'プッシュ通知のチャンネル名',
            importance: Importance.max, // 通知の重要度の設定
            icon: android?.smallIcon,
          ),
        ),
        payload: json.encode(message.data),
      );
    }
  });

  // ローカルから表示したプッシュ通知をタップした場合の処理を設定
  flutterLocalNotificationsPlugin.initialize(
    const InitializationSettings(
      android: AndroidInitializationSettings(
          '@mipmap/ic_launcher'), //通知アイコンの設定は適宜行ってください
      iOS: DarwinInitializationSettings(),
    ),
    onDidReceiveNotificationResponse: (details) {
      if (details.payload != null) {
        final payloadMap =
            json.decode(details.payload!) as Map<String, dynamic>;
        debugPrint(payloadMap.toString());
      }
    },
  );
}

// class MyApp extends StatelessWidget {
// 〜 以下は初期状態のままなので省略 〜

上記の設定を追加で行うことでアプリ停止時、バックグラウンド、フォアグランド起動時のすべてで同じように通知を行うことができました。

終わりに


ここまでで無事Androidでの通知設定が完了しました。
次回は残りのiOSの設定について記載する予定です。
こちらも設定手順が多いですが、よろしければお付き合いください。
お読みくださりありがとうございました。


執筆者プロフィール:sakurai
SHIFT DAAEグループ所属の開発エンジニアです。
MOTHERとSTYLEだとSTYLE派。

《この公式ブロガーのほかの記事》

\もっと身近にもっとリアルに!DAAE公式Twitter/


\お問合せはお気軽に/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら