⛈️

FlutterでCloud Functionsを使う

2023/01/27に公開

トリガー関数ってなに?

Cloud Functionsと呼ばれているサーバーレスなフレームワークで、使うことができる機能です。AWSのLambdaと同じですね。

  • 特徴
    • サーバーがいらない.
    • 関数をFirebase内に配置するだけ.
    • イベントが起きると関数が実行される.

イベントとは?

FireStoreにデータが追加された、ユーザーが登録されたといった様々なユースケースがあります。push通知が有名な機能ですね。

こちらの動画が参考になりました
https://www.youtube.com/watch?v=WrzvBulgBi0

今回作成したアプリはこんな感じです
https://youtu.be/5XdFws9quio

今回作成したCloud Functionsの関数は、TypeScriptで作成しました。最近はTypeScriptで、コード書くのが、当たり前の時代になりつつある😅
こちらが完成品のコード
https://github.com/sakurakotubaki/FlutterCloudFunctions

皆さんは同じものを作るときは、Flutterのプロジェクトを作成して、プロジェクト直下のディレクトリで、Cloud Functionsの環境構築をやってください。私は別に作ったものを後で、Flutterのプロジェクトに追加したので、ディレクトリの構造が異なります。

Flutter側の設定

今回は、Flutterアプリはデータの追加だけしかやってないです🙇‍♂️
Zennのスクラップに投稿したCloud Functionsのソースコードと同じものを使ってるので、Firebaseコンソールで操作するだけでも、どんな仕組みで動いているのかは見れるので、今回は省きました。

Flutterアプリ側の役割

データをFireStoreに追加すると、Cloud Functionsがデータの変化を感知して、関数を実行できるかを検証します。これだけのために、Flutterアプリ用意しました💁

main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:functions_app/firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FunctionsTest(),
    );
  }
}

class FunctionsTest extends StatefulWidget {
  const FunctionsTest({Key? key}) : super(key: key);

  
  State<FunctionsTest> createState() => _FunctionsTestState();
}

class _FunctionsTestState extends State<FunctionsTest> {
  final TextEditingController nameC = TextEditingController();
  final TextEditingController bookC = TextEditingController();
  final TextEditingController priceC = TextEditingController();

  Future<void> addBook(String nameC, String bookC, String priceC) async {
    await FirebaseFirestore.instance.collection('buyers').add({
      "name": nameC,
      "book": bookC,
      "price": priceC,
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('本の購入履歴を追加する'),
      ),
      body: Center(
        child: Column(
          children: [
            TextField(
              controller: nameC,
              decoration: InputDecoration(
                hintText: "購入者の名前を入力",
              ),
            ),
            TextField(
              controller: bookC,
              decoration: InputDecoration(
                hintText: "本の名前を入力",
              ),
            ),
            TextField(
              controller: priceC,
              decoration: InputDecoration(
                hintText: "本の値段を入力",
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  addBook(nameC.text, bookC.text, priceC.text);
                },
                child: Text('購入履歴を追加'))
          ],
        ),
      ),
    );
  }
}

Cloud Functionsを準備する

私、DefaultのESLintの設定を適用したせいで、;や""、タブのスペースを行数を合わせたりと、コードを書くのが難しくなって作るの大変だったので、遊ぶだけなら、ESLintは入れないほうがいいです😵

環境構築

Cloud Functionsは、有料のプランでないと使えないので、Firebaseを有料プランに変更してください。
1ヶ月200万回までなら、無料みたい?
超えたら有料!
https://cloud.google.com/functions/pricing?hl=ja

公式の通りに進めれば、Cloud Functionsをインストールできます。
今回は、TypeScriptを使ってるので、こちらを選んでください。
特別な設定をしなければ、20230127-functions/functions/src/index.tsのコードを書くだけで、OKです💁‍♀️

Cloud Functionsの公式
動画とドキュメントだけで、使い方はなんとなくですが理解しました。
https://firebase.google.com/docs/functions/get-started?hl=ja

FireStoreの公式
Node.jsの書き方がそのまま使えるので、そこまで難しくはないです。これが、ReactとNext.jsだったら、もっと書くの難しいです😇
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

最初は、何書いてるのかわからなかったですが、やってることは単純で、Node.jsで、FireStoreを操作するコードをCloud Functionsのトリガー関数の中に書いてるだけです。

  • やること
    • 追加
    • 更新
    • 削除

buyersコレクションのデータに変更があれば、変更を監視しているCloud Functionsが、イベント発生時に、よく聞く、トリガーって現象が起きて関数が実行されます。

/* eslint-disable @typescript-eslint/no-unused-vars */
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();

const db = admin.firestore();

// buyersコレクションにデータが追加されたら、purchaseHistoryに、コレクションデータを追加する.
// eslint-disable-next-line max-len, @typescript-eslint/no-unused-vars
exports.onUserCreate = functions.firestore.document("buyers/{buyerId}").onCreate(async (snap, context) => {
  const newValues = snap.data();
  // eslint-disable-next-line max-len
  await db.collection("purchaseHistory").add({
    name: `本の購入者は、${newValues.name}`,
    book: `購入した本は、${newValues.book}`,
    // price: `本の値段: ${newValues.buyerData.price}`,
    price: newValues.price,
  });
});

// buyersコレクションのデータが更新されたら、purchaseHistoryのコレクションのデータを更新する.
// eslint-disable-next-line max-len, @typescript-eslint/no-unused-vars
exports.onUserUpdate = functions.firestore.document("buyers/{buyerId}").onUpdate(async (snap, context) => {
  const newValues = snap.after.data();

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const updatePromises: any[] = [];

  const snapshot = await db.collection("purchaseHistory").get();
  // eslint-disable-next-line arrow-parens
  snapshot.forEach(doc => {
    updatePromises.push(db.collection("purchaseHistory").doc(doc.id).update({
      name: `本の購入者は、${newValues.name}`,
      book: `購入した本は、${newValues.book}`,
      price: newValues.price,
    }));
  });
  await Promise.all(updatePromises);
});

// buyersコレクションのデータが削除されたら、purchaseHistoryのコレクションのデータを削除する.
// eslint-disable-next-line max-len, @typescript-eslint/no-unused-vars
exports.onPostDelete = functions.firestore.document("buyers/{buyerId}").onDelete(async (snap, context) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const deletePromises: any[] = [];

  const snapshot = await db.collection("purchaseHistory").get();

  // eslint-disable-next-line arrow-parens
  snapshot.forEach(doc => {
    deletePromises.push(db.collection("purchaseHistory").doc(doc.id).delete());
  });
  await Promise.all(deletePromises);
});

まとめ

ESLintの設定が、Defaltの物だと結構チェックが厳しいので、後から導入して、;つけてとか、""にしてねぐらいがいいのかも知れません🫠
TypeScript久しぶりに勉強して、ESLintとprettierの設定をしたリポジトリも共有しておきます。
後から、この設定にしておけばESLint入れたら、出てくるエラー連発せずに済んだかも知れないですね😇
もっと綺麗なコードを書きたい...
https://github.com/sakurakotubaki/TypeScriptLesson/tree/develop

Discussion