FlutterでCloud Functionsを使う
トリガー関数ってなに?
Cloud Functionsと呼ばれているサーバーレスなフレームワークで、使うことができる機能です。AWSのLambdaと同じですね。
- 特徴
- サーバーがいらない.
- 関数をFirebase内に配置するだけ.
- イベントが起きると関数が実行される.
イベントとは?
FireStoreにデータが追加された、ユーザーが登録されたといった様々なユースケースがあります。push通知が有名な機能ですね。
こちらの動画が参考になりました
今回作成したアプリはこんな感じです
今回作成したCloud Functionsの関数は、TypeScriptで作成しました。最近はTypeScriptで、コード書くのが、当たり前の時代になりつつある😅
こちらが完成品のコード
皆さんは同じものを作るときは、Flutterのプロジェクトを作成して、プロジェクト直下のディレクトリで、Cloud Functionsの環境構築をやってください。私は別に作ったものを後で、Flutterのプロジェクトに追加したので、ディレクトリの構造が異なります。
Flutter側の設定
今回は、Flutterアプリはデータの追加だけしかやってないです🙇♂️
Zennのスクラップに投稿したCloud Functionsのソースコードと同じものを使ってるので、Firebaseコンソールで操作するだけでも、どんな仕組みで動いているのかは見れるので、今回は省きました。
Flutterアプリ側の役割
データをFireStoreに追加すると、Cloud Functionsがデータの変化を感知して、関数を実行できるかを検証します。これだけのために、Flutterアプリ用意しました💁
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万回までなら、無料みたい?
超えたら有料!
公式の通りに進めれば、Cloud Functionsをインストールできます。
今回は、TypeScriptを使ってるので、こちらを選んでください。
特別な設定をしなければ、20230127-functions/functions/src/index.tsのコードを書くだけで、OKです💁♀️
Cloud Functionsの公式
動画とドキュメントだけで、使い方はなんとなくですが理解しました。
FireStoreの公式
Node.jsの書き方がそのまま使えるので、そこまで難しくはないです。これが、ReactとNext.jsだったら、もっと書くの難しいです😇
最初は、何書いてるのかわからなかったですが、やってることは単純で、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入れたら、出てくるエラー連発せずに済んだかも知れないですね😇
もっと綺麗なコードを書きたい...
Discussion