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

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

はじめに

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

今回はFirebaseを使った機械学習モデルの推論についてです。

目次

シリーズの内容

回数 内容 リンク
第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

FirebaseとFlutterで使用できる機械学習の推論

FlutterとFirebaseを使った機械学習の推論には、オンデバイスで推論する方法と、クラウド上で推論する方法があります。

オンデバイスの推論でFirebaseを使うというのは、カスタムTensorFlowLiteモデルFirebase MLで配信してローカルで推論することです。 実際の推論にはtflite_flutter, ML Kitなどを使用します。 カスタマイズされたTFモデルをFirebaseで配信するメリットはユーザーがアプリをアップデートせずに最新のモデルを使用できるという点です。

クラウドの推論でFirebase(Google Cloud)を使うというのは、Cloud Vision AICloud Natural Languageなどで推論するということです。 現在のところ、Flutter向けにはAPIが提供されていないので、各OS向けのAPIを組み合わせる必要があります。

なお、上記以外にも、他サービスの機械学習モデルを使う方法があります。

FirebaseによるカスタムTensorflowLiteモデル配信の導入方法

自分で学習させたTFのカスタムモデルをTFLiteに変換したファイルを用意します。

今回は画像認識のタスクを実行するため、TensorFlow HubからImagenetの画像分類学習モデルを取得しました。 このとき、ライセンスとダウンロードするファイルの種類に気をつけてください。

ファイルを用意できたら、Firebase ConsoleMachine Learningからモデルをデプロイします。

公式では推論にtflite_fluttertfliteが推奨されていましたが、私の開発環境ではtflite_flutter等をインポートした状態でアプリをビルド出来なかったので、ML Kitで実験しました。

ML KitでTFLiteのカスタムモデルを使用できるタスクには、Image LabelingまたはObject Detection and Trackingがあります。 今回はImage Labelingの推論をしますので、google_ml_kitgoogle_mlkit_image_labelingも導入します。

dependencies:
  google_ml_kit: ^0.11.0
  google_mlkit_image_labeling: ^0.3.0

Firebase MLからモデルのダウンロードは、google_mlkit_image_labelingパッケージに入っているFirebaseImageLabelerModelManagerを使用します。

final bool response =
    await FirebaseImageLabelerModelManager().downloadModel(modelname);
final options = FirebaseLabelerOption(
        confidenceThreshold: 0.5, modelName: modelname, maxCount: 3);
_imageLabeler = ImageLabeler(options: options);

image_picker等で写真のパスがわかれば、基本的なラベル推論に必要なコードは2行です。

final InputImage inputImage = InputImage.fromFilePath(path);
final List labels = await _imageLabeler.processImage(inputImage);

推論された結果からラベルを取り出します。

String labelText = '';
for (final label in labels) {
  labelText += '\nLabel: ${label.label}';
}

アプリで確認してみます。 推論は出来ているみたいです。 ただ、思っていた結果とは違ったので、改善の余地がありそうです。

コード全文

前回からの変更点です。

  • Firebase MLを実装
  • ml_pageページを追加
  • main.dartml_pageページに遷移するボタン追加
  • その他、コードの変更

ml_page.dart

/// Flutter
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:google_mlkit_image_labeling/google_mlkit_image_labeling.dart';

/// Firebase
import 'package:firebase_ml_model_downloader/firebase_ml_model_downloader.dart';

/// Providerの初期化
final imageStateProvider = StateProvider<File?>((ref) => null);
final textStateProvider = StateProvider<String?>((ref) => null);

class MLPage extends ConsumerStatefulWidget {
  const MLPage({Key? key}) : super(key: key);

  @override
  MLPageState createState() => MLPageState();
}

class MLPageState extends ConsumerState<MLPage> {
  /// 画像ラベル分類問題の定義
  late ImageLabeler _imageLabeler;
  ImagePicker? _imagePicker;

  /// ImageLabelerの初期化
  @override
  void initState() {
    super.initState();
    _initializeLabeler();
    _imagePicker = ImagePicker();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('画像からラベル分類'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          /// 画像が存在すれば画像を表示
          /// そうでなければ画像アイコンを表示
          ref.watch(imageStateProvider) != null
              ? SizedBox(
                  height: 400,
                  width: 400,
                  child: Stack(
                    fit: StackFit.expand,
                    children: <Widget>[
                      Image.file(ref.watch(imageStateProvider)!),
                    ],
                  ),
                )
              : const Icon(
                  Icons.image,
                  size: 200,
                ),
          ElevatedButton(
            child: const Text('写真を選択'),
            onPressed: () => _getImage(ImageSource.gallery),
          ),

          Container(
            padding: const EdgeInsets.all(10),
            child: Text(ref.watch(imageStateProvider) == null
                ? ''
                : ref.watch(textStateProvider) ?? ''),
          ),
        ],
      ),
    );
  }

  /// ラベラーの初期化
  void _initializeLabeler() async {
    /// モデルの名前はFirebase MLにアップロードした名前
    const modelname = 'Image';

    /// FirebaseからML kitに学習モデルを読込ませるため、FirebaseImageLabelerModelManagerを使う
    /// 参考 : https://pub.dev/documentation/google_mlkit_image_labeling/latest/
    final bool response =
        await FirebaseImageLabelerModelManager().downloadModel(modelname);
    print(response);

    /// ラベラーのオプションを設定し、読込
    final options = FirebaseLabelerOption(
        confidenceThreshold: 0.5, modelName: modelname, maxCount: 3);
    _imageLabeler = ImageLabeler(options: options);
  }

  /// 画像を選択し推論を実行
  Future _getImage(ImageSource source) async {
    /// 画像選択
    final pickedFile = await _imagePicker?.pickImage(source: source);

    /// 有効な画像が選択できたら推論
    if (pickedFile != null) {
      /// ファイルのパスを取得
      final path = pickedFile.path;
      ref.read(imageStateProvider.state).state = File(path);
      final inputImage = InputImage.fromFilePath(path);

      /// ラベル分類推論実行
      final labels = await _imageLabeler.processImage(inputImage);

      /// ラベルの分類が成功した場合、ラベルのテキストを生成
      if (inputImage.inputImageData?.size != null &&
          inputImage.inputImageData?.imageRotation != null) {
      } else {
        String labelText = '';
        for (final label in labels) {
          labelText += '\nLabel: ${label.label}';
        }
        ref.read(textStateProvider.state).state = labelText;
      }
    }
  }
}

GitHubのページを貼ります。

github.com

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

はじめに

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

今回はターゲットを絞り込んだメッセージを送信することができるFirebase In-App Messagingについてです。 このサービスはモバイル(iOS, Android)でのみ使用できます。

目次

シリーズの内容

回数 内容 リンク
第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

導入方法

プロジェクトにfirebase_in_app_messagingを導入するため、pubspec.yamlに追記します。

In-App Messagingはサーバーからのメッセージの取得を1日に1回だけしか行わないため、テストではFirebase installation ID (FID)を使って実験します。

dependencies:
  firebase_in_app_messaging: ^0.6.0+14
  firebase_app_installations: ^0.1.0+14

Flutterでは、firebase_in_app_messagingを読み込むことでIn-App Messagingを使用できます。

import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';

FIDの取得FirebaseInstallationsを使用します。

import 'package:firebase_app_installations/firebase_app_installations.dart';

void getFID() async {
  String id = await FirebaseInstallations.instance.getId();
  print('id : $id');
}

Firebase Consoleから配信テストをします。 準備ができたら、先ほど控えたFIDを入力し、デバイス配信テストを行います。

配信後、デバッグ端末で一度ホーム画面に戻り、再度アプリを開くと、アプリ内メッセージを確認できます。

なお、以下のようなエラーメッセージが発生した場合、Firebase In-App Messaging APIが無効になっていますので、エラー文の通り、Google Cloud Platform Firebase In-App Messaging APIにアクセスし、APIを有効にする必要があります。

PERMISSION_DENIED: Firebase In-App Messaging API has not been used in project *** before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseinappmessaging.googleapis.com/overview?project=*** then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

コード全文

前回からの変更点です。

  • In-App Messagingを実装

main.dart抜粋

/// Flutter関係のインポート
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase関係のインポート
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';

/// メイン
void main() async {
  /// クラッシュハンドラ
  runZonedGuarded<Future<void>>(() async {
    /// Firebaseの初期化
    WidgetsFlutterBinding.ensureInitialized();

    /// FCMのバックグランドメッセージを表示
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

    await Firebase.initializeApp(
      name: isAndroid || isIOS ? 'counterFirebase' : null,
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// クラッシュハンドラ(Flutterフレームワーク内でスローされたすべてのエラー)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// クラッシュハンドラ(Flutterフレームワーク内でキャッチされないエラー)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// MaterialAppの設定
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends ConsumerState<MyHomePage> {
  @override
  void initState() {
    super.initState();

    /// FCMのパーミッション設定
    FirebaseMessagingService().setting();

    /// FCMのトークン表示(テスト用)
    FirebaseMessagingService().fcmGetToken();

    /// Firebase ID取得(テスト用)
    FirebaseInAppMessagingService().getFID();
  }

  @override
  Widget build(BuildContext context) {
...
  }
}


class FirebaseInAppMessagingService {
  void getFID() async {
    String id = await FirebaseInstallations.instance.getId();
    print('id : $id');
  }
}

GitHubのページを貼ります。

github.com

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