FlutterでFirebase Realtime Databaseを使ってみた

FlutterでFirebase Realtime Databaseを使って実装してみましたのでご紹介します。簡単なチャットアプリを例に説明していきます。
2018.08.23

大阪オフィスの山田です。AmazonでのEssential phoneのセールを見逃して泣きそうでした。今回はFirebase Realtime DatabaseをFlutterから使用してみたいと思います。Firebase Realtime Databaseを使うこと自体初めてです。

今回やること

Firebase Realtime Databaseに対して、一覧取得、追加をFlutterで作ったアプリから行います。ライブラリ、firebase_databaseを使用します。Exampleも存在しますが、今回はもっと簡潔にした実装をしていきます。イメージとしてはこのようなアプリです。

入力したら、リストに追加されていくだけのシンプルなチャットです(果たしてチャットと呼んで良いか疑わしいレベル)。もちろん、他の端末から見ると、投稿がリストに表示されるようにします。

開発環境

flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.26.1)
[✓] Connected devices (1 available)

flutter --version

Flutter 0.5.1 • channel beta • https://github.com/flutter/flutter.git
Framework • revision c7ea3ca377 (3 months ago) • 2018-05-29 21:07:33 +0200
Engine • revision 1ed25ca7b7
Tools • Dart 2.0.0-dev.58.0.flutter-f981f09760

準備

各プラットフォームの設定

Android、iOS、各プラットフォームのプロジェクトをFirebase上に作成し、google-service.jsonGoogleService-Info.plistをプロジェクトに追加します。こちらの記事の「準備」の章で各プラットフォームで設定をしていますのでご参照ください。

Firebase Realtime Databaseの準備

Databaseを用意します。Firebase Consoleにログインして、Databaseを選択します。

Realtime Databaseまでスクロールして、「データベースを作成」を選びます。

テストモードで作成し、誰でも読み書きできるようにします。

データベースが作成されました。

実装

データ構造を定義

まず、データ構造を定義します。1つのチャットメッセージはmessageと投稿日時dateを持つようにします。それとは別にデータ上のキーを持ちます。簡単。以下のように定義しました。databaseのデータはDataSnapshotとして取得できるので、DataSnapshotからインスタンスを生成できるようにしています。

class ChatEntry {
  String key;
  DateTime dateTime;
  String message;

  ChatEntry(this.dateTime, this.message);

  ChatEntry.fromSnapShot(DataSnapshot snapshot):
    key = snapshot.key,
    dateTime = new DateTime.fromMillisecondsSinceEpoch(snapshot.value["date"]),
    message = snapshot.value["message"];

  toJson() {
    return {
      "date": dateTime.millisecondsSinceEpoch,
      "message": message,
    };
  }
}

画面の実装

画面全体の実装をまず記載しておきます。  

class FirebaseChatPage extends StatefulWidget {
  @override
  _FirebaseChatPageState createState() => new _FirebaseChatPageState();
}

class _FirebaseChatPageState extends State<FirebaseChatPage> {
  final _mainReference = FirebaseDatabase.instance.reference().child("messages");
  final _textEditController = TextEditingController();

  List<ChatEntry> entries = new List();  // チャッtのメッセージリスト

  @override
  initState() {
    super.initState();
    _mainReference.onChildAdded.listen(_onEntryAdded);
  }

  _onEntryAdded(Event e) {
    setState(() {
      entries.add(new ChatEntry.fromSnapShot(e.snapshot));
    });
  }

  // 画面全体のビルド
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: new Text("Firebase Chat")
      ),
      body: Container(
        child: new Column(
          children: <Widget>[
            Expanded(
              child: 
              ListView.builder(
                padding: const EdgeInsets.all(16.0),
                itemBuilder: (BuildContext context, int index) {
                  return _buildRow(index);
                },
                itemCount: entries.length,
              ),
            ),
            Divider(height: 4.0,),
            Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildInputArea()
            )
          ],
        )
      ),
    );
  }
  
  // 投稿されたメッセージの1行を表示するWidgetを生成
  Widget _buildRow(int index) {
    return Card(
      child: ListTile(
        title: Text(entries[index].message)
      )
    );
  }
  
  // 投稿メッセージの入力部分のWidgetを生成
  Widget _buildInputArea() {
    return Row(
      children: <Widget>[
        SizedBox(width: 16.0,),
        Expanded(
          child: TextField(
            controller: _textEditController,
          ),
        ),
        CupertinoButton(
          child: Text("Send"),
          onPressed: () {
            _mainReference.push().set(ChatEntry(DateTime.now(), _textEditController.text).toJson());
            _textEditController.clear();
            // キーボードを閉じる
            FocusScope.of(context).requestFocus(new FocusNode());
          },
        )
      ],
    );
  }
}

Realtime databaseからの受信部分

final _mainReference = FirebaseDatabase.instance.reference().child("messages");  // 1
  
  @override
  initState() {
    super.initState();
    _mainReference.onChildAdded.listen(_onEntryAdded);  // 2
  }

  _onEntryAdded(Event e) {  // 3
    setState(() {
      entries.add(new ChatEntry.fromSnapShot(e.snapshot));
    });
  }
  1. まず、databaseのルートの下のmessagesという子を参照するようにします。
  2. messagesに子が追加されたら_onEntryAddedをコールするように設定します。
  3. _onEntryAddedで追加された子を取得できるのでSnapshotからChatEntryクラスを生成してリストに追加して、画面を再描画します。

Realtime databaseへの送信部分

    // 投稿メッセージの入力部分のWidgetを生成
  Widget _buildInputArea() {
    return Row(
      children: <Widget>[
        SizedBox(width: 16.0,),
        Expanded(
          child: TextField(
            controller: _textEditController,
          ),
        ),
        CupertinoButton(
          child: Text("Send"),
          onPressed: () {
            _mainReference.push().set(ChatEntry(DateTime.now(), _textEditController.text).toJson());  // 1
            _textEditController.clear();
            // キーボードを閉じる
            FocusScope.of(context).requestFocus(new FocusNode());
          },
        )
      ],
    );
  }
  1. 送信ボタンが押された時に、textEditControllerから入力されたテキストを取得し、現在時刻と共にChatEntryクラスを生成して、送信します。

投稿した後、Firebase ConsoleでDatabaseを見ると以下のようになっていました。
ちゃんとデータが書き込まれていますね。

リストについて

今回の実装ではListViewを仕様していますが、今回使用したライブラリにはFirebaseAnimatedListというクラスが用意されています。こちらはRealtime Databaseのリファレンスをセットしておくと、追加、更新、削除の時に自動で描画してくれる便利なクラスです。仕様に合致する場合は積極的に使って良いと思います。

最後に

いかがだったでしょうか。Flutterを触っていて詰まった時、iOSであればあたりがつくのですが、Androidの方は経験不足からなかなかどこに問題があるのかパッとイメージがつかず四苦八苦しております。これからもがんばろー。

参考文献

  • firebase_database: 使用したライブラリのGithubリポジトリ
  • Firebase Realtime Databaseの公式ドキュメント