概要
Cloud Functions for Firebaseはトリガーされたイベントに応じて、バックエンドコードを自動的に実行できるサービスです。
Cloud Functions for Firebaseはjavascript
とtypescript
での記述に対応しており、サーバの管理やスケーリングせずにバックエンドを実装できます。
公式ドキュメントはこちら。
準備
本チャプターは準備編と前回までのチャプターが完了していることが前提です。
Cloud FunctionsをJavaScript環境で実行するためにはNode.js
環境が必要なので、まだ導入していない場合はnvmを使ってインストールします。
導入方法
firebase-tools
をインストールします。
npm install -g firebase-tools
プロジェクトを初期化します。
firebase login
が出来ている環境では、functions
とその他必要なツールを初期化します。
今回は言語にJavaScript
を選択しました。
firebase init functions
初期化出来たら、プロジェクト内に新しくfunctions
フォルダが作成されます。
関数の作成
Cloud Functions
を実行するための関数を、functions/index.js
に記述していきます。
必要なモジュールをインポートします。
const functions = require("firebase-functions");
関数の定義です。
関数を呼び出すには大きく3つあります。
- アプリから直接呼び出す方法
- HTTPリクエスト経由で関数を呼び出す方法
- スケジュール設定で呼び出す方法
今回のカウントアップ機能は、アプリから直接呼び出して使用するため、バックエンド側ではonCall
トリガーを使用します。
ついでに、UIDを呼び出せるかも実験してみます。
exports.functionsTest = functions.https.onCall((data, context) => {
/// 数値読み取り
const firstNumber = data.firstNumber;
const secondNumber = data.secondNumber;
/// 計算実行
const addNumber = firstNumber + secondNumber;
/// ついでに、UIDも呼び出せるか実験
const contextUid = context.auth.uid;
return { addNumber:addNumber, contextUid:contextUid }
});
エミュレータでテスト
無限ループになっていないかなどを確認するため、デプロイする前にローカルエミュレータでテストします。
なお、App Check
を使用している場合は、エミュレーターが実行できず、App Checkのデバッグプロバイダーを使用する必要がありますが、現時点では公式サイトにも記載されてある通り、Dart APIが提供されていないので、各実行環境でデバッグプロバイダーを使用しなければなりません。
エミュレータの起動にはJava
が必要なので、Open JDKをインストールしておきます。
ローカルエミュレータをインストール&初期化し、必要に応じてfirebase init ***
で各プラグインをインストールします。
firebase init emulators
ローカルエミュレータを使うときは、Flutter側のmain
関数にuseFunctionsEmulator
の設定をします。
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Ideal time to initialize
FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);
...
インストールできたらemulators:start
でエミュレータを起動し、ブラウザでhttp://localhost:4000/
(デフォルトの場合)を開きます。
firebase emulators:start
デプロイ
問題なければ、本番環境にデプロイします。
Firebase ConsoleからFunctions
に移動し、「開始」を選択します。
firebase deploy --only functions:functionsTest
Flutterアプリからの呼び出し
Cloud FunctionsをFlutterプロジェクトに適用するため、pubspec.yaml
にcloud_functions
を導入します。
dependencies:
cloud_functions: ^3.3.2
functions
を実行するコードを書きます。
import 'package:cloud_functions/cloud_functions.dart';
/// Cloud Functionsの実行
void addNumber() async {
try {
/// カウントアップの関数の読み出し
final result = await FirebaseFunctions.instance
.httpsCallable('functionsTest')
.call({'firstNumber': _number, 'secondNumber': 1});
_number = result.data['addNumber'];
print(result.data['contextUid']);
} on FirebaseFunctionsException catch (error) {
print(error.code);
print(error.details);
print(error.message);
}
}
カウントアップするためだけにCloud Functionsを使う贅沢なアプリが完成しました。
コード全文
前回からの変更点です。
- Cloud Functionsの関数を追加
- cloud_functionsページを追加
- その他、コードの変更
functions/index.js
const functions = require("firebase-functions");
exports.functionsTest = functions.https.onCall(async(data, context) => {
/// 数値読み取り
const firstNumber = data.firstNumber;
const secondNumber = data.secondNumber;
/// 計算実行
const addNumber = firstNumber + secondNumber;
/// ついでに、UIDも呼び出せるか実験
const contextUid = context.auth.uid;
return { addNumber:addNumber, contextUid:contextUid }
});
main.dart
/// Flutter関係のインポート
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Firebase関係のインポート
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:counter_firebase/remote_config_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';
/// 他ページのインポート
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';
import 'package:counter_firebase/auth_page.dart';
import 'package:counter_firebase/firestore_page.dart';
import 'package:counter_firebase/realtime_database_page.dart';
import 'package:counter_firebase/cloud_storage.dart';
import 'package:counter_firebase/cloud_functions_page.dart';
import 'package:counter_firebase/ml_page.dart';
/// プラットフォームの確認
final isAndroid =
defaultTargetPlatform == TargetPlatform.android ? true : false;
final isIOS = defaultTargetPlatform == TargetPlatform.iOS ? true : false;
/// FCMバックグランドメッセージの設定
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Handling a background message: ${message.messageId}');
}
void main() async {
/// クラッシュハンドラ
runZonedGuarded<Future<void>>(() async {
/// Firebaseの初期化
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
/// FCMのバックグランドメッセージを表示
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
/// クラッシュハンドラ(Flutterフレームワーク内でスローされたすべてのエラー)
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
/// Cloud Functionsのローカルエミュレータ設定
// FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);
/// runApp w/ Riverpod
runApp(const ProviderScope(child: MyApp()));
},
/// クラッシュハンドラ(Flutterフレームワーク内でキャッチされないエラー)
(error, stack) =>
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}
/// Providerの初期化
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter() : super(0);
/// カウントアップ
void increment() => state++;
}
/// MaterialAppの設定
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter Firebase',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
/// ホームページ画面
class MyHomePage extends ConsumerStatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends ConsumerState<MyHomePage> {
void initState() {
super.initState();
/// FCMのパーミッション設定
FirebaseMessagingService().setting();
/// FCMのトークン表示(テスト用)
FirebaseMessagingService().fcmGetToken();
/// Firebase ID取得(テスト用)
FirebaseInAppMessagingService().getFID();
}
Widget build(BuildContext context) {
/// ユーザー情報の取得
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
ref.watch(userEmailProvider.state).state = 'ログインしていません';
} else {
ref.watch(userEmailProvider.state).state = user.email!;
}
});
return Scaffold(
appBar: AppBar(
title: const Text('My Homepage'),
),
body: ListView(
padding: const EdgeInsets.all(10),
children: <Widget>[
/// ユーザ情報の表示
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.person),
Text(ref.watch(userEmailProvider)),
],
),
/// ページ遷移
const _PagePushButton(
buttonTitle: 'ノーマルカウンター',
pagename: NormalCounterPage(),
),
const _PagePushButton(
buttonTitle: 'クラッシュページ',
pagename: CrashPage(),
),
const _PagePushButton(
buttonTitle: 'Remote Configカウンター',
pagename: RemoteConfigPage(),
),
const _PagePushButton(
buttonTitle: '機械学習ページ',
pagename: MLPage(),
),
const _PagePushButton(
buttonTitle: '認証ページ',
pagename: AuthPage(),
bgColor: Colors.red,
),
/// 各ページへの遷移(認証後利用可能)
/// 認証されていなかったらボタンを押せない状態にする
FirebaseAuth.instance.currentUser?.uid != null
? const _PagePushButton(
buttonTitle: 'Firestoreカウンター',
pagename: FirestorePage(),
bgColor: Colors.green,
)
: const Text('Firestoreカウンターを開くためには認証してください。'),
FirebaseAuth.instance.currentUser?.uid != null
? const _PagePushButton(
buttonTitle: 'Realtime Databaseカウンター',
pagename: RealtimeDatabasePage(),
bgColor: Colors.green,
)
: const Text('Realtime Databaseカウンターを開くためには認証してください。'),
FirebaseAuth.instance.currentUser?.uid != null
? const _PagePushButton(
buttonTitle: 'Cloud Storageページ',
pagename: CloudStoragePage(),
bgColor: Colors.green,
)
: const Text('Cloud Storageページを開くためには認証してください。'),
FirebaseAuth.instance.currentUser?.uid != null
? const _PagePushButton(
buttonTitle: 'Cloud Functionsページ',
pagename: CloudFunctionsPage(),
bgColor: Colors.green,
)
: const Text('Cloud Functionsページを開くためには認証してください。'),
],
),
);
}
}
/// ページ遷移のボタン
class _PagePushButton extends StatelessWidget {
const _PagePushButton({
Key? key,
required this.buttonTitle,
required this.pagename,
this.bgColor = Colors.blue,
}) : super(key: key);
final String buttonTitle;
final dynamic pagename;
final Color bgColor;
Widget build(BuildContext context) {
return ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(bgColor),
),
child: Container(
padding: const EdgeInsets.all(10),
child: Text(buttonTitle),
),
onPressed: () {
AnalyticsService().logPage(buttonTitle);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => pagename),
);
},
);
}
}
/// Analyticsの実装
class AnalyticsService {
/// ページ遷移のログ
Future<void> logPage(String screenName) async {
await FirebaseAnalytics.instance.logEvent(
name: 'screen_view',
parameters: {
'firebase_screen': screenName,
},
);
}
}
/// Firebase Cloud Messageの設定
class FirebaseMessagingService {
FirebaseMessaging messaging = FirebaseMessaging.instance;
/// webとiOS向け設定
void setting() async {
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}');
}
void fcmGetToken() async {
/// モバイル向け
if (isAndroid || isIOS) {
final fcmToken = await messaging.getToken();
print(fcmToken);
}
// web向け
else {
final fcmToken = await messaging.getToken(
vapidKey: FirebaseOptionMessaging().webPushKeyPair);
print('web : $fcmToken');
}
}
}
class FirebaseInAppMessagingService {
void getFID() async {
String id = await FirebaseInstallations.instance.getId();
print('id : $id');
}
}
cloud_functions_page.dart
/// Flutter
import 'package:flutter/material.dart';
/// Firebase
import 'package:cloud_functions/cloud_functions.dart';
class CloudFunctionsPage extends StatefulWidget {
const CloudFunctionsPage({Key? key}) : super(key: key);
CloudFunctionsPageState createState() => CloudFunctionsPageState();
}
class CloudFunctionsPageState extends State<CloudFunctionsPage> {
/// 初期化
int _number = 0;
/// Cloud Functionsの実行
void addNumber() async {
try {
/// カウントアップの関数の読み出し
final result = await FirebaseFunctions.instance
.httpsCallable('functionsTest')
.call({'firstNumber': _number, 'secondNumber': 1});
_number = result.data['addNumber'];
} on FirebaseFunctionsException catch (error) {
print(error.code);
print(error.details);
print(error.message);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cloud Functionsページ'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_number',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
addNumber();
});
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
上記のコードが含まれるGitHubのページです。
参考