Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Flutter と Firebase と Cloud Functions

こんにちは。中山です。

Google 社が提供するサーバレス技術である、 Cloud Functions
今回は、 Cloud Functions for Firebase を用いて、Clound Functions の機能を Firebase 経由で利用し、さらにそれを Flutter で作成したモバイルアプリから呼び出すことで、Flutter と Firebase 、そして Cloud Functions の連携を、お手軽に試してみたいと思います。


目次:

 

時代はサーバレス、なのか

触発されましたのはこちらの記事。
www.orangeitems.com

Docker や Kubernetes を用いたコンテナに関連する技術、また、 AWS Lambda や Cloud Functions などに代表されるサーバレスの技術。
日々トレンドに挙がり続けているこれらの技術ですが、上記の記事でも言及がありますとおり、すでにこれらがメインストリームである、というわけではなく、まだまだ昔ながらの「アプリケーションサーバと DB サーバを用意して・・・」という構成で運用されている環境は少なくないのではないかと想像しています。

とはいえ、それがすなわち、コンテナ・サーバレスの技術はキャッチアップしなくてもよい、ということにはなりません。
今回は、サーバレス技術として Cloud Functions を取り上げてみまして、Flutter 、 Firebase と連携させながら、果たしてどれだけメリットを見いだせるのか、というのを確認してみます。
 

Flutter と Firebase の連携

Flutter を用いたモバイルアプリの開発、また、Flutter と Firebase の連携については、当ブログで以前ご紹介した、これらの記事が参考になるでしょう。
iti.hatenablog.jp
iti.hatenablog.jp

Flutter を使ってアプリ開発したり、またそこから Firebase と連携したり、というのをいくつか試した感想としては、

  • Flutter でのモバイルアプリ開発、(むちゃくちゃ凝ったことをしない、というのであれば、)ものすごく早い。
  • iOS アプリも、Android と同様なマテリアルデザインのままでよければ、特に iOS 用の実装というのを意識することはない。
  • Firebase との連携も、非常にハードルが低い。

というものでした。
いろいろ凝ったことをやろうとすると、この構成ではまかないきれないところが出てくるのかもしれませんが、ごくごく一般的なアプリを作る、という限りでは、大きなメリットがありそうです。
 

Cloud Functions 利用時の Firebase 料金プラン

今回のパターンでは、Flutter で作成するモバイルアプリから Cloud Functions の関数を呼び出すことになりますため、Cloud Functions のアウトバウンドネットワーキングが有効である必要があるようでした。
このため、無料の Spark プランではなく、(無料枠も含む)従量制の Blaze プランを適用しています。
firebase.google.com
 

Cloud Functions の実装

今回は、こちらの 公式チュートリアル をほぼそのまま使わせてもらいました。
以下の 2つの関数を作成します。

  • addMessage()  : テキスト値を受け取る URL を公開し、そのテキスト値を Realtime Database に書き込みます。
  • makeUppercase()  : Realtime Database の書き込みでトリガーされ、テキストを大文字に変換します。

このあと Flutter で作成するアプリからの呼び出しを考慮して、このチュートリアルのコードを少しだけ調整しました。
調整後の functions/index.js は以下のようになります。

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

const admin = require('firebase-admin');
admin.initializeApp();

exports.addMessage = functions.https.onCall((data, context) => {
    const original = data.text;
    console.log('original:', original);

    admin.database().ref('/messages').push({ original: original}).then((snapshot) => {
        console.log('messages/original:', snapshot.ref.toString());
    })

    return { result: original };
});

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original').onCreate((snapshot, context) => {
    const original = snapshot.val();
    console.log('Uppercasing', context.params.pushId, original);

    const uppercase = original.toUpperCase();

    return snapshot.ref.parent.child('uppercase').set(uppercase);
});

 

チュートリアル実行後、Firebase コンソールには以下のように関数が登録されます。
 f:id:IntelligentTechnology:20181031153917p:plain:w520
 

この addMessage 関数を呼び出すと、

  1. Firebase の Realtime Database の messages/{自動で割り振られるID}/original というパスに、関数のパラメータで指定された文字列が登録され、
  2. そのデータの登録をトリガーにして、makeUppercase 関数が呼び出されて、original パスに登録されている文字列を大文字にしたのち、uppercase パスにその値を登録する

という処理が行われることになります。
addMessage 関数を直接呼び出した場合は、Firebase の Realtime Database のデータの状態はこのようになります。
 f:id:IntelligentTechnology:20181031155340p:plain:w400
 

ここまでできましたところで、今度はこの関数を、Flutter で作るモバイルアプリから呼び出してみることにします。
 

Flutter アプリの実装

プロジェクトの作成と登録

Android Studio から、Flutter アプリのプロジェクトを新規作成し、その情報を Firebase コンソールにも登録します。
このあたりの具体的な手順については、当ブログの記事「 Flutter と Firebase と Google ML Kit 」もご参照ください。
今回は cloud_functions_lesson という名前でプロジェクトを作成し、それを Firebase コンソールに登録しました。
 f:id:IntelligentTechnology:20181031163836p:plain:w320
 

プラグインの導入

Flutter アプリから Cloud Functions の関数を呼び出せるようにするために、 cloud_functions というプラグインを利用します。
Flutter アプリプロジェクトのディレクトリ内にある pubspec.yaml ファイルの dependencies ブロックの部分に、以下のように cloud_functions の記述を追加します。

dependencies:
  flutter:
    sdk: flutter
  cloud_functions: ^0.0.4+1

それから、Android プラットフォーム用に、 android/build.gradle ファイルの buildscript → dependencies の部分に google-services の定義を追加します。

buildscript {
    ・・・

    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath 'com.google.gms:google-services:4.1.0'
    }
}

もうひとつ、android/app/build.gradle ファイルの末尾に、以下を追加します。

apply plugin: 'com.google.gms.google-services'

このあたりは、もうちょっと Flutter 側で、自動でうまいことしてくれるとうれしいなぁ、というところです。
 

コードの実装

Flutter アプリ側のコードは、この cloud_functions プラグインの Example ページにありましたコードを、全面的に参考にさせてもらいました。
lib/main.dart ファイルの中身は以下のようになります。

// Copyright 2018, the Chromium project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:cloud_functions/cloud_functions.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _response = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Cloud Functions example app'),
        ),
        body: Center(
          child: Container(
            margin: const EdgeInsets.only(top: 32.0, left: 16.0, right: 16.0),
            child: Column(
              children: <Widget>[
                RaisedButton(
                  child: const Text('ADD MESSAGE'),
                  onPressed: () async {
                    try {
                      final dynamic resp = await CloudFunctions.instance.call(
                        functionName: 'addMessage',
                        parameters: <String, String>{
                          'text': 'hello world!',
                        },
                      );
                      print(resp);
                      setState(() {
                        _response = resp['result'];
                      });
                    } on CloudFunctionsException catch (e) {
                      print('caught firebase functions exception');
                      print(e.code);
                      print(e.message);
                      print(e.details);
                    } catch (e) {
                      print('caught generic exception');
                      print(e);
                    }
                  },
                ),
                Text('Response: $_response'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

 

今回は、ボタンクリックで、固定の文字列( hello world! )を Cloud Functions の関数に渡して、Firebase の Realtime Database に登録する、という実装です。
この中で、実際に Cloud Functions の関数を呼び出しているのは(ボタンタップのイベントハンドラ内の)以下の部分。

try {
  final dynamic resp = await CloudFunctions.instance.call(
    functionName: 'addMessage',
    parameters: <String, String>{
      'text': 'hello world!',
  },
);

関数名を指定して、そのパラメータを設定します。これだけ。
 

アプリの実行

今回は Android スマートフォンでこのアプリを起動してみます。
以下のような画面が表示され、
 f:id:IntelligentTechnology:20181101112407p:plain:w320
 

ADD MESSAGE ボタンをタップしますと、Firebase の Realtime Database に、無事に以下のようにデータが登録されました。
 f:id:IntelligentTechnology:20181101112611p:plain:w400
 

Cloud Functions のメリット・デメリット

サーバサイドの処理としては、

  • 関数を登録するのみで、
  • アプリケーションサーバやウェブサーバのメンテナンスもいらず、
  • アクセスの増減によるスケーリングもうまいことやってくれる、

となりますと、メリットしかないようにも思えます。
しかしながら、たとえば以下のスライドでも言及されていますように、

www.slideshare.net

  • ステートレスであるがゆえの非効率性(状態を保持しようとすると、外部のデータストアに頼るしかない)
  • オンデマンド起動であるがためのオーバーヘッド負荷などの課題

などがあって、

  • ゲームなど、リアルタイム性を要求されるバックエンド
  • 長時間のバッチ処理

には向いていないだろう、という見解もあるようです。

実際のところ、適材適所でサーバレスの技術を投入する、ということなんだろう、とは思いますが、一方、従来の、自分でアプリケーションサーバをたてて、サーバサイドのアプリケーションをデプロイして、とやっていたところの多くは、実は Cloud Functions などのサーバレスのアーキテクチャで置き換え可能なのではないかとも感じています。

Cloud Functions をはじめ、サーバレス技術は今後もどんどん進歩していく分野だと思いますので、しっかりキャッチアップしていき、人間ができるだけ楽をすることができるような運用環境づくりを目指したいものです。