💡

【Flutter】Cloud Functions x Firestore x FCMでpush通知を送る

4 min read

ゴール

flutterアプリで通知したい内容と日付を設定すると、その日付に合わせて通知が送られてくる。

結論

通知が送られてきた。

環境

  • macOS Big Sur (チップ : M1)
  • Flutter : 2.2.3
  • Dart : 2.13.4

全体の流れ

flutter側でFCM通知設定とFirestoreへのデータ保存機能、CloudFunctions側でFirestoreのデータ参照と通知送信機能を実装します。

【アプリで通知を受け取る準備をする】

  1. Flutterプロジェクト側の設定
  2. Dartファイルに記述するコード
  3. 通知が送られてくるかの確認

【実際に通知を送るきっかけ(トリガー)を作る】

  1. Cloud Functionsを始める
  2. Cloud Functions関数の記述

【アプリで通知を受け取る準備をする】

1. Flutterプロジェクト側の設定

以下の記事を参考にしました。

https://zuma-lab.com/posts/flutter-fcm-push-notify-settings

2. Dartファイルに記述するコード

動作確認できたコード例をGitHubにアップロードしました。

https://github.com/nasubibocchi/notify

.dartのファイル構成

  • main.dart : 通知設定とUI
  • message.dart : Firestoreとやりとりするデータのクラス
  • addPush.dart : 通知を設定するページ(UI)
  • model.dart : addPush.dartで設定した通知をFirestorenに保存するモデル

通知設定はmain.dart内で完結しています。
サンプルコードを調べたところ、StatelessWidgetで書かれているものが多く、FCMのパッケージ(firebase_messaging)のExampleでも同様でした。

アプリは2ページの簡易的な構造になっています。
mainpagesubpage

【実際に通知を送るきっかけ(トリガー)を作る】

1. Cloud Functionsを始める

今回初めてCloudFunctionsを利用しました。
利用開始の手順は公式ドキュメントにも記載されています。

https://firebase.google.com/docs/functions/get-started?hl=ja

あまりつまづくところはなかったですが、公式ドキュメントに記載がない部分で私が手間取った点↓

初回は bash or zsh にnpmのパスを通す手順が必要になった。

2. Cloud Functions関数の記述

公式ドキュメントに従って『プロジェクトの初期化』(firebase init functions)が成功したら、『functions』フォルダ内にindex.jsが生成されます。
(javascript or typescriptでtypescriptを選択したら .ts)
今回の場合、実際に編集が必要だったのはこの『index.js』ファイルだけでした。

関数はexport.以下で定義します。
この関数の数がアカウント全体で3つより多くなると課金されていくようです。

実際のコード全体

動作確認できたコードは以下です。
確認しやすいように3分ごとに通知が来るようになっていますが、24時間毎とかにも設定できるようです。


const functions = require('firebase-functions')
const admin = require('firebase-admin');
const { DataSnapshot } = require('firebase-functions/lib/providers/database');
admin.initializeApp()
const firestore = admin.firestore()


//push通知実行メソッド
const pushMessage = (fcmToken, text) => ({
  notification: {
      title: '新しいオファーを受信しました。',
      body:  `${text}`,
  },
  apns: {
      headers: {
          'apns-priority': '10'
      },
      payload: {
          aps: {
              badge: 9999,
              sound: 'default'
          }
      }
  },
  data: {
      data: 'test',
  },
  token: fcmToken
});

///関数
exports.mySendMessages = functions.region('asia-northeast1')
  .runWith({ memory: '512MB' })
  .pubsub.schedule('every 3 minutes')//関数を実行する時間間隔が設定できる
  .timeZone('Asia/Tokyo')
  .onRun(async (context) => {

    // 秒を切り捨てた現在時刻
    const now = (() => {
      let s = admin.firestore.Timestamp.now().seconds
      s = s - s % 60
      return new admin.firestore.Timestamp(s, 0)
    })()

    //秒を切り捨てた昨日の時刻(動作確認のため広めに設定)
    const yesterday = (() => {
      let s = admin.firestore.Timestamp.now().seconds
      s = s - 86400
      return new admin.firestore.Timestamp(s, 0)
    })()

    //秒を切り捨てた明日の時刻
    const tomorrow = (() => {
      let s = admin.firestore.Timestamp.now().seconds
      s = s + 43200
      return new admin.firestore.Timestamp(s, 0)
    })()


    ///時刻の確認
    console.log('now', now.toDate())
    console.log('yesterday', yesterday.toDate())
    console.log('tomorrow', tomorrow.toDate())
    
    
    //try(うまくデータが取れた!)
    const pushDataRef = firestore.collection('pushData');
    const snapshot = await pushDataRef.where('postAt', '>=', yesterday).where('postAt', '<=', tomorrow).get();
    if (snapshot.empty) {
      console.log('No matching documents.');
      return;
    }
    snapshot.forEach(doc => {
      console.log(doc.id, '=>', doc.data());
      console.log(doc.data()['postAt']);
      //通知送信
      const token = doc.data()['fcmToken']
      const title = doc.data()['title']
      admin.messaging().send(pushMessage(token, title))
    });     
  })//.onRun

Discussion

ログインするとコメントできます