snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Cloud Functions編)

はじめに

Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第12回目です。

今回はトリガーされたイベントに応じて、バックエンドコードを自動的に実行できるCloud Functions for Firebaseについてです。 Cloud Functions for Firebaseはjavascripttypescriptでの記述に対応しており、サーバの管理やスケーリングせずにバックエンドを実装できるサービスです。

目次

シリーズの内容

回数 内容 リンク
第0回 準備編 ブログ
第1回 Analytics ブログ
第2回 Firebase Crashlytics ブログ
第3回 Firebase Performance Monitoring ブログ
第4回 Firebase Remote Config ブログ
第5回 Firebase Authentication ブログ
第6回 Cloud Firestore ブログ
第7回 Firebase Realtime Database ブログ
第8回 Cloud Storage for Firebase ブログ
第9回 Firebase Cloud Messaging ブログ
第10回 Firebase In-App Messaging ブログ
第11回 Firebase ML ブログ
第12回 Cloud Functions for Firebase イマココ
第13回 Firebase Hosting ブログ
第14回 Firebaseのその他のサービス ブログ

開発環境

項目 内容
PC Macbook Air(M1)
Flutter 3.0.1
Firebase 11.0.1
FlutterFire 0.2.2+2
デバッグバイス Android 12(APIレベル31), Chrome

準備

準備編が完了出来ているものとします。

snova301.hatenablog.com

Cloud FunctionsではNode.js環境が必要なので、まだ導入していない場合はnvmを使ってインストールします。

また、Cloud FunctionsはBlazeプランでのみ使用できるので、料金プランをアップデートする必要があります。 なお、ある一定回数までは無料枠が適用されます。

導入方法

npmが使えるようなったら、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");

関数の定義です。 関数を呼び出すにはアプリから直接呼び出す方法と、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

Cloud FunctionsをFlutterプロジェクトに適用するため、pubspec.yamlcloud_functionsを導入します。

dependencies:
  cloud_functions: ^3.2.15

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を使う贅沢なアプリが完成しました。

なお、参考までに、私の場合、App Checkを使わないとエラーでfunctionsが実行出来ませんでした。

App Checkは一度開始すると解除できないため検証のしようがなく、公式サイトには推奨としか記載されていなかったので、あくまで参考程度で記載しておきます。

コード全文

前回からの変更点です。

  • Cloud Functions for Firebaseを実装
  • cloud_functionsページを追加
  • その他、コードの変更

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);

  @override
  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'];
      print(result.data['contextUid']);
    } on FirebaseFunctionsException catch (error) {
      print(error.code);
      print(error.details);
      print(error.message);
    }
  }

  @override
  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),
      ),
    );
  }
}

functions/index.js

const functions = require("firebase-functions");

exports.functionsTest = functions.https.onCall(async(data, context) => {
  /// App Checkの実施
  if (context.app == undefined) {
        throw new functions.https.HttpsError(
            'failed-precondition',
            'The function must be called from an App Check verified app.')
      }

  /// 数値読み取り
  const firstNumber = data.firstNumber;
  const secondNumber = data.secondNumber;

  /// 計算実行
  const addNumber = firstNumber + secondNumber;

  /// ついでに、UIDも呼び出せるか実験
  const contextUid = context.auth.uid;

  return { addNumber:addNumber,  contextUid:contextUid }
});

GitHubのページを貼ります。

github.com

参考

github.com

firebase.google.com

github.com

Google Play and the Google Play logo are trademarks of Google LLC.