Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Cloud Functions編)
はじめに
Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第12回目です。
今回はトリガーされたイベントに応じて、バックエンドコードを自動的に実行できるCloud Functions for Firebaseについてです。
Cloud Functions for Firebaseはjavascript
とtypescript
での記述に対応しており、サーバの管理やスケーリングせずにバックエンドを実装できるサービスです。
目次
シリーズの内容
回数 | 内容 | リンク |
---|---|---|
第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 |
準備
準備編が完了出来ているものとします。
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.yaml
にcloud_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のページを貼ります。
参考
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 |
準備
準備編が完了出来ているものとします。
FirebaseとFlutterで使用できる機械学習の推論
FlutterとFirebaseを使った機械学習の推論には、オンデバイスで推論する方法と、クラウド上で推論する方法があります。
オンデバイスの推論でFirebaseを使うというのは、カスタムTensorFlowLiteモデルをFirebase MLで配信してローカルで推論することです。
実際の推論にはtflite_flutter
, ML Kit
などを使用します。
カスタマイズされたTFモデルをFirebaseで配信するメリットはユーザーがアプリをアップデートせずに最新のモデルを使用できるという点です。
クラウドの推論でFirebase(Google Cloud)を使うというのは、Cloud Vision AIやCloud Natural Languageなどで推論するということです。 現在のところ、Flutter向けにはAPIが提供されていないので、各OS向けのAPIを組み合わせる必要があります。
なお、上記以外にも、他サービスの機械学習モデルを使う方法があります。
FirebaseによるカスタムTensorflowLiteモデル配信の導入方法
自分で学習させたTFのカスタムモデルをTFLiteに変換したファイルを用意します。
今回は画像認識のタスクを実行するため、TensorFlow HubからImagenetの画像分類学習モデルを取得しました。 このとき、ライセンスとダウンロードするファイルの種類に気をつけてください。
ファイルを用意できたら、Firebase ConsoleのMachine Learning
からモデルをデプロイします。
公式では推論にtflite_flutterやtfliteが推奨されていましたが、私の開発環境ではtflite_flutter
等をインポートした状態でアプリをビルド出来なかったので、ML Kitで実験しました。
ML KitでTFLiteのカスタムモデルを使用できるタスクには、Image LabelingまたはObject Detection and Trackingがあります。
今回はImage Labeling
の推論をしますので、google_ml_kitとgoogle_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.dart
にml_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のページを貼ります。
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 |
準備
準備編が完了出来ているものとします。
導入方法
プロジェクトに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のページを貼ります。