はじめに
初投稿です。
Flutterでアプリ制作を行っていた際にプッシュ通知機能を実装しようとして色々調べた際、様々な情報が錯綜しており、どの記事が正しいのかが分からなくなって色んなことを試した結果、大口の沼にはまり込んでしまったため、自分用の備忘録として残します。
同じ問題に直面している人の助けとなれば幸いです。
前提条件
リモートプッシュ通知を送る方法は様々あると思いますが、今回はプロジェクトの要件として利用していたAWS Lambda(以下Lambda)からFirebase Cloud Messaging(以下FCM)を利用してAndroid端末にプッシュ通知を送っていきます。今回は、LambdaはNode.js(TypeScript)で実装します。
iOS端末にプッシュ通知を送る際は、Appleの開発者サイトやXCodeで追加設定が必要になるのですが、今回は割愛します。
この記事では以下のことを行っている前提で説明していきます。
- AWSアカウント・Firebaseアカウント作成済み
- Flutterプロジェクト作成済み
事前準備
Firebase
Firebaseの準備から行います。Firebaseのプロジェクトをまだ作成していない場合は、新たなプロジェクトを作成しましょう。やり方は名前を付けて、アナリティクスの構成を決定するだけなので割愛します。
プロジェクトにアプリを追加
作成したFirebaseのプロジェクトに、Androidアプリを追加します。次の画像の赤枠で囲まれたアイコンをクリックします。
FlutterプロジェクトでAndroidのパッケージ名を確認・入力し、「アプリを登録」をクリックします。
すると構成ファイルをダウンロードできるようになるため、ダウンロードしてFlutterのプロジェクトに追加します。構成ファイルは、/[project_root]/android/appの中に保存します。
③のステップはスキップしてこれで追加完了です。
iOSアプリも同時に行う場合は、プロジェクトのトップからiOSアプリの追加をクリックし、同じ手順を踏みます。構成ファイルは、/[project_root]/ios/Runnerの中に保存します。
サービスアカウント
続いてFirebaseをLambdaで使えるように設定を行います。Firebaseのプロジェクトトップページの左側のメニュー上部から、「プロジェクトの概要」の右にある歯車アイコンをクリックし、「プロジェクトの設定」をクリックします。
「サービスアカウント」のタブをクリックし、「新しい秘密鍵の生成」をクリックします。
するとjsonファイルがダウンロードされますので、これをNode.jsプロジェクトのルートに保存しましょう。
Flutterの設定
Flutter側でプッシュ通知を有効にするためにFlutter側でも設定を行います。「FlutterFire」を利用すると必要な設定を自動で行ってくれるため、今回はこちらで設定を進めます。実装時は以下の公式ドキュメントを参考にしながら実装しました。
https://firebase.flutter.dev/docs/overview/
FlutterFire
FlutterFireとは、その名の通りFlutterとFirebaseを繋ぎ合わせるためのFlutter用のプラグインです。
FlutterFireの導入
FlutterFireを利用するにはfirebase_core
プラグインを導入する必要があるため、コマンドプロンプトやターミナルでFlutterプロジェクトフォルダに移動してから、以下のコマンドを実行します。
flutter pub add firebase_core
FlutterFireの初期化
FlutterFireを初期化するためには、FlutterFire CLIを使います。FlutterFire CLIはFirebase CLIに依存しているため、Firebase CLIをインストールする必要があります。Firebase CLIをまだ導入していない場合は以下のドキュメントに従って導入してください。
https://firebase.google.com/docs/cli
FlutterFireを導入します。コマンドプロンプトやターミナルで以下のコマンドを実行します。
dart pub global activate flutterfire_cli
インストールが完了したら、プロジェクトフォルダで以下のコマンドを実行します。
flutterfire configure
実行すると、先程設定したFirebaseのプロジェクトが表示されますので選択します。選択後、どのプラットフォームで設定を行うかを問われますので、必要なプラットフォームを選択して、続行します。
設定が完了するとfirebase_options.dart
というファイルが作成されているはずです。これを/[project_root]/lib/main.dartに追記します。
まずはfirebase_core
とfirebase_options.dart
をインポートして
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
メイン関数を修正します。
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 中略..
runApp(MyApp());
}
FlutterFireを初期化出来ない場合
flutterfire configure
のコマンドが失敗してしまう場合などは、Firebase CLIでしっかりログインが出来ているかどうかを確認してみてください。
プッシュ通知の実装(Flutter)
こちらを参考に実装して行きます。
https://firebase.flutter.dev/docs/messaging/usage
まずは、firebase_messaging
のプラグインを追加します。
flutter pub add firebase_messaging
/[project_root]/lib/main.dartにそのまま通知の処理を書いていきます。
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; // 追加
import 'firebase_options.dart';
// 中略..
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 中略..
// FCMから通知を受け取るための処理
FirebaseMessaging messaging = FirebaseMessaging.instance; // 追加
// 通知の許可をもらう
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
); // 追加
print('User granted permission: ${settings.authorizationStatus}'); // 追加
runApp(MyApp());
}
Foreground, Background, Terminated
メッセージを受け取る際、アプリが最前面に表示されている時(Foreground)、アプリが裏で待機している時(Background)、アプリが終了している時(Terminated)でそれぞれの処理を書かなければいけないようです。
Foregroundメッセージ
アプリがForeground状態のときは、onMessage
ストリームを使ってメッセージを受け取ります。通知の許可をもらう処理の後に以下を追記します。
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Got a message whilst in the foreground!');
print('Message data: ${message.data}');
if(message.notification != null) {
print('Message also contained a notification: ${message.notification}');
}
// 中略..
// メッセージを受け取った際に行いたい処理等
});
デフォルトでは Foreground時にメッセージを受け取っても通知は表示されません。 Foreground時にメッセージを受け取った際にもプッシュ通知を表示したい場合は、flutter_local_notifications
プラグイン等を使ってローカルプッシュ通知を表示させるように実装を行います。実装する場合は、以下を参考に実装してみてください。
https://firebase.flutter.dev/docs/messaging/notifications/#foreground-notifications
Background, Terminatedメッセージ
アプリがBackgroundまたはTerminatedの状態のときはonBackgroundMessage
ハンドラを利用します。ドキュメントによると、ハンドラは以下の条件を満たしていなければならないようです。
- アノニマス関数でない
- top-level関数である(初期化が必要なクラス関数等ではない)
main関数の外とForegroundメッセージの処理の後に処理を追記します。
// 追加
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Firestore等の他のFirebaseのサービスを使う場合は、initializeApp()で初期化を行う。
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
// 中略..
// メッセージを受け取った際に行いたい処理等
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 中略..
// FCMから通知を受け取るための処理
FirebaseMessaging messaging = FirebaseMessaging.instance;
// 通知の許可をもらう
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
print('User granted permission: ${settings.authorizationStatus}');
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Got a message whilst in the foreground!');
print('Message data: ${message.data}');
if(message.notification != null) {
print('Message also contained a notification: ${message.notification}');
}
// 中略..
// メッセージを受け取った際に行いたい処理等
});
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // 追加
runApp(MyApp());
}
Foregroundメッセージとは違い、Background, Terminatedメッセージはメッセージを受け取ったタイミングでプッシュ通知が端末に表示されます。
FCM Token
サーバーがどの端末にプッシュ通知を送信するかを特定するために、端末側でトークンを発行する必要があります。通知の許可をもらった後に以下の処理を追記します。
String? token = await messaging.getToken();
print(token);
実際のサービスでは、トークンはユーザーの認証を行い(必要であれば)、サーバー側に送ってデータベースへ保存しておく必要がありますが、今回はデモなので、トークンをログに出力しておきます。このトークンは後ほどLambda側の実装で使います。
プッシュ通知の実装(Lambda)
続いてLambda側の実装に入ります。まずはFirebase Admin Node.js SDKを導入しましょう。
コマンドプロンプト・ターミナルでNode.jsのプロジェクトのルートへ移動し、コマンドを実行します。
npm i firebase-admin
/[project-root]/src/FirebaseCloudMessagingService.tsを作成します。また、事前準備のサービスアカウントでダウンロードしたjsonファイルを読み込みます。FCM Tokenも忘れずに記載します。
import admin from "firebase-admin";
import firebaseAccountCredentials from "../service-account.json";
class FirebaseCloudMessagingService {
constructor() {
// adminが既に初期化されている場合は初期化しないようにする
if(admin.apps.length === 0) {
const serviceAccount = firebaseAccountCredentials as admin.ServiceAccount;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}
}
public async sendNotification() {
const res = await admin.messaging().send({
token: "{Flutterで取得したFCM Token}",
notification: {
title: "Hello, World!",
body: "リモートプッシュ通知です!",
},
android: {
priority: "high",
},
apns: {
payload: {
aps: {
contentAvailable: true,
},
},
headers: {
"apns-push-type": "background",
"apns-priority": "5", // Must be `5` when `contentAvailable` is set to true.
},
},
});
console.log(res);
}
}
export default FirebaseCloudMessagingService;
android
だのapns
だの訳のわからないプロパティがくっついていますが、これはそれぞれAndroidとiOSで認識されるプロパティのようで、メッセージを重要性の高いメッセージとして認識させるための記述のようです。
今回はmessaging
のsend
関数を使いましたが、sendMultiCast
という関数もあり、これは複数のトークンに対して同じメッセージを一括で送信することができます。token
というプロパティをtokens
に変更して、配列でトークンを指定してあげると使えます。
では、Lambdaが実行されたらメッセージが送られるように実装して行きます。/[project-root]/src/index.tsを実装して行きます。
import { APIGatewayProxyCallbackV2, APIGatewayProxyEventV2, Context } from "aws-lambda";
import FirebaseCloudMessagingService from "./FirebaseCloudMessagingService";
export async function handler(event: APIGatewayProxyEventV2, context: Context, callback: APIGatewayProxyCallbackV2): Promise<any> {
FirebaseCloudMessagingService fcm = new FirebaseCloudMessagingService();
await fcm.sendNotification();
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: "Message has been sent",
}),
});
}
これでLambdaが実行されれば、Flutterアプリに"Hello, World!"というタイトルでプッシュ通知が届くはずです。
終わりに
以上がLambdaを使ってFCMを介しFlutterにプッシュ通知を送る実装になります。実際のサービスレベルではまだまだ必要な実装が多いと思いますが、リモートプッシュ通知を送るために何が必要になるのか等、この記事で知ってもらえたら幸いです。