Flutter×Firestore×FCM(iOS)で簡易的なトーク通知

# はじめに
こんにちは、とろろです。ととろじゃないです。
Life is Tech!でメンターをしており、普段は iPhoneプログラミングコース と Webサービスコース を担当しています。
最近は学生ベンチャーの企業でエンジニアもしていて、 そこで**通知付きトーク機能** のついたiOSアプリを（超短期間で）作ってほしいと言われたので、爆速で開発するために **Flutter × Firestore × FCM** をフル活用した経験があるのですが、今回の**Advent Calender**ではその技術の用法についてまとめていこうと思います。
なお、本記事の解説の中心はトーク通知機能の実装であり、チャット機能のDB構成はコーディングの方法に依るため今回は簡易的な設計となっております。

# 開発環境（使用ライブラリ） / DB構成
- Flutter
- Firebase(Firestore)
- FCM(Firebase cloud messaging)

Firebase導入やFlutterのセットアップの方法は省略しますので、次のページや記事を参考にしてみてください。
- [Flutterセットアップ](https://zenn.dev/kboy/books/ca6a9c93fd23f3/viewer/5232dc)
- [Firebase導入](https://zenn.dev/namioto/articles/026077bdee5eaa)
- [FCMセットアップ](https://firebase.google.com/docs/cloud-messaging/flutter/client)

# 通知機能の実装
次に、通知機能の実装で必要となる部分のコーディングをしていきます。
細かいデータベースの組み方は各々のプロジェクトによって変わるため、その都度変更して頂けたらと思います。

### ① FCMにおけるトークンの取得
FCMにおけるトークンとは、**Firebase Cloud Messaging**を利用する際に、各固有デバイスに付与される値のことです。
(公式の参考ドキュメントは[こちら](https://firebase.google.com/docs/cloud-messaging/android/client?hl=jp#retrieve-the-current-registration-token)から)
このトークンを取得するメソッドと、usersに保存する用のメソッドをそれぞれ作成します。
今回はそれ専用のクラスを作成して、staticメソッドとして定義していきます。
```message.dart
static Future<String> getToken() async {
    String token = "-1";
    await FirebaseMessaging.instance.getToken().then((doc) {
        token = doc!;
    });
    return token; // 取得したtokenを返し値にする
}

static Future<void> saveToken({required String uid, required String token}) async {
    await FirebaseFirestore.instance
        .collection("users")
        .doc(uid)
        .update({
            "user_token": token
        });
}
```

### ② iOS通知のための設定
次の[参考ページ](https://zuma-lab.com/posts/flutter-fcm-push-notify-settings)にしたがって、iOS側の設定を済ませます。
基本的にはFCMの推奨通りに、 **p12** ではなく **p8** で作成するようにしましょう。
Notificationの設定をしたら、①と同じクラスに通知許可を出すメソッドを定義していきます。
```message.dart
static Future<void> requestPermission() async {
    await FirebaseMessaging.instance.requestPermission(
        alert: true, // デバイス上で通知を表示するかを決めるプロパティ
        announcement: true, // AirPodsに接続時にSiriが通知内容を読み上げるかを決めるプロパティ
        badge: true, // ホーム画面のアイコン横に赤いバッジを表示させるかを決めるプロパティ
        carPlay: true, // carPlayに接続時に通知を表示するかを決めるプロパティ
        sound: true, // 通知が届いた時にサウンドを鳴らすかを決めるプロパティ
    );
}
```
他にもiOS12以降のデバイス向けに`provisional`プロパティも用意されています。詳しくは[こちら](https://firebase.flutter.dev/docs/messaging/permissions/#provisional-authorization)を参照にしてみてください。ここまで完成したら、アプリ自体に通知が飛ぶための機能は完成しています。
`main.dart`で上で定義したメソッドを呼び出し、[Firebase Messaging](https://firebase.google.com/products/cloud-messaging?hl=ja)で通知テストを飛ばしてみましょう。
**※シミュレーターでは通知が飛ばないので実機で試してください。**

### ③ firebase functionsでFirestoreが更新された時に通知を飛ばす
次に、ターミナル上でプロジェクトファイルに移動し、**firebase functions**の設定をしていきます。
基本的には初期値で大丈夫ですが、次の設定項目だけ切り替えておきましょう。
```shell
firebase init

¦ こちらにチェックを入れる
 (*) Functions: Configure and deploy Cloud Functions 
¦

¦ どちらでも大丈夫ですが、今回は TypeScript を用いて開発します。
? What language would you like to use to write Cloud Functions?
  JavaScript
> TypeScript
¦

¦ コーディングの方法を統一するESLintを導入するかの質問です。どちらでも良いですが、今回はnにします。
? Do you want to use ESLint to catch probable bugs and enforce style? (Y/n)
n
¦

```
設定が終わると、プロジェクト直下に`functionsディレクトリ`が作成されます。
その中にある`src/index.ts`に通知を飛ばすための設定を書いていきます。
```index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

admin.initializeApp();
const db = admin.firestore();

// documentの引数はfirestoreのDBの作り方に書き換えてください。
// 今回はcontext　サブコレクションの中で新たにドキュメントが作成された時をイベントハンドラの対象としています。
exports.messagePushNotify = functions.firestore.document(chats/{roomId}/context/{contextId})
    .onCreate(async (snap, context) => {
        const roomId = context.params.roomId
        const contextId = context.params.contextId

        // snapshotから{contextId}ドキュメントの中身を取得します。
        const title = snap.data().name
        const message = snap.data().context
        const contactUserUid = snap.data().contact_uid

        // ペイロードの設定をします。
        // titleは通知のタイトル、bodyは通知の本文を表します。
        const payload = {
            notification: {
                title: title,
                body: messsage,
                sound: "default",
            }
        }

        // usersコレクションに保存しているtokenを取得します。
        // userのtokenをどこに保存するかに依るので、その都度書き換えてください。
        const userRef = db.collection("users").doc(contact_user_uid);
        const userDoc = await userRef.get();
        try {
            const token = userDoc.get("user_token")
            // このmessagingメソッドで通知を飛ばします。
            admin.messaging().sendToDevice(token, payload);
        } catch (error) {
            console.log(`Error!: ${error}`);
        }
    })
    
```
これで完成です！あとは、firestoreの指定部分が作られる度に通知がデバイスに飛ぶようになります。

# まとめ
FCMを使えばフロントドリブンな通知処理を簡単に実行できちゃう嬉しさがあります！
通知がどのような処理によって行われているのかを知らなくともApple大先生とGoogle大先生のNotification, Firebase周りのパッケージでサクッと作れるため、短期間・少人数で機能が完成できるのがとても助かりました。
より便利なFirebase周りのパッケージや機能を、また探して遊んでみようかな〜〜〜と思います。

明日はベテラン Life is Techer!である**りょう**さんの記事になります！
それでは、引き続き**東海 Life is Tech! Advent Calender**をお楽しみください！:v: