23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DiverseAdvent Calendar 2019

Day 22

FlutterとFirestoreを使い始めて一ヶ月がたった

Last updated at Posted at 2019-12-22

はじめに

こんにちは、 @tacksman です。
Diverse Advent Calendar 2019 22日目の記事です。

自分がDiverseにジョインしてから一ヶ月ほど経ったので、その間にFlutterやFirestoreを使っていて躓いた点などを紹介しようかと思います。

前提

自分はここ数年はKotlinでのAndroidアプリ開発がすべてでしたが、それより前には Cordovaとreact-redux
を使用したiOSアプリの開発に参加していたこともありました。
今回は非同期処理となるFirestoreへのアクセスにStreamBuilderを使用していますが、こちらの使い方の詳細などについては取り扱わないでいきます。

FlutterとFirestoreの組み合わせ

おなじみのFirestoreをFlutterで扱う際のポイントです。
自分も久しぶりにFirestoreを扱うこともあり、Web上のドキュメントを読んだりしましたが、Flutter+Firestoreについて解説している日本語のドキュメントはあまりない上、公式のドキュメントを読むほうが参考になることが多い気がしています。
そこで、そういった部分であまりふれられていない箇所を中心に取り上げておきます。

Firestoreからのデータの取得

ここではFlutterでの非同期処理にStreamStreamBuilderを使用しています。
StreamBuilderを使用してFirestoreからデータを取得し、それをもとに表示するコンポーネントを作成するには以下のようになります。

StreamBuilder(
  stream: Firestore.instance.collection("messages").snapshots(),
  builder: (context, snapshot) {
    /// snapshot.dataでQuerySnapshotを取り出して求める型に置換したりする
  },
)

Repositoryパターンを使用しているので、画面側のロジックで型変換したくないよお……という方は、Repositoryで置換後にStreamに包んで返し、それをStreamBuilderのstreamにいれましょう。

Firestoreからのデータ購読

次は購読です。
Firestore上のデータを購読するには以下のようなコードを実行することで、/messagesのパスにあたるドキュメント(もしくはコレクション)の購読を開始できます。

Firestore.instance.collection("messages").snapshots().listen((data) {
  /// DocumentSnapshotな引数dataとして、購読したいドキュメントのスナップショットが落ちてくる
})

listen内のFunctionでスナップショットから実用する型へ変換することになります。
listen()は返り値としてStreamSubscription<QuerySnapshot>を返しますが、こちらは購読の終了を行うときにcancel()をコールするためのものです。
購読を行っている画面を閉じるときなど、購読を止めたいタイミングでcancel()を実行するようにしておきましょう。

さて、それではどのように画面側へ購読したデータを届けましょうか。
PublishSubject<T>使用するなどいくつかの方法がありますが、disposeし忘れに注意をはらないといけなかったり、データの流れを追いにくいです。
しかし画面側でStreamBuilderを使っていると、listenを使わず素直なデータの流れで実装できます。


class Repository {
...

Stream<List<Message>> watch(String messageId) {
  Firestore.instance.document("messages/${messageId}")
    .snapshots()
    .map((querySnapshot) { // MessageのQuerySnapshot
      // List<Message>に変換する
    });
}

...
}


class MessagePage extends StatefulWidget {
  ...
}

class MessagePageState extends State<MessagePage> {
 String messageId;
 Repository repository = Repository;
 ...
 @override
 Widget build(BuildContext context) {
  repository.watch(messageId);
  return StreamBuilder(
   stream: repository.watch(),
   builder: ...
  );
 }

 ...
}

Stream<QuerySnapshot>に生えているlisten()をコールして手動で購読を行ってもいいですが、そちらでは変換したデータを画面にもっていく部分や購読のキャンセルをしっかり行う必要が出てくるなど煩雑になってしまいます。
しかしStreamBuilderを使用しているときはStreamBuilder内部でlisten()cancel()を行ってくれるため、比較的簡単に購読を実装できます。

Firestoreへのデータ追加

取得、購読ときたら次はFirestoreへのデータ追加についてです。
しかしsetDataするだけなら公式ドキュメントを読めばいいので、それ以外の部分について書いていきます。

データ取得のところで使ったMessageデータで考えてみましょう。
例えばチャットをする機能の場合、チャットルームに対してメッセージのリストを持つ形を考えて、以下のような構造にしてみます。
lastUpdatedAtは最後にチャットが投稿された日時を示すTimestampです。

ChatRoom
class ChatRoom {
 List<Message> messages;
 Timestamp lastUpdatedAt;
 ....
}

class Message {
 String text;
 ...
}

そして特定のチャットルームのFirestore上のパスを"chatRooms/${chatRoomId}"、特定のメッセージまでのパスを"chatRooms/${chatRoomId}/messages/${messageId}"としておきます。

上記の前提の場合だと、lastUpdatedAtの更新とmessagesへの追加を同時に行わなければなりません。
このようなときはbatchを使用しましょう。

async
final batch = Firestore.instance.batch();

batch.setData(
  Firestore.instance
    .document("chatRooms/${chatRoomId}")
    .collection("messages")
    .document(),
  {
    /// Messageを表現するMap
    ...
  }
);

batch.update(
  Firestore.instance
    .document("chatRooms/${chatRoomId}/lastUpdatedAt"),
  Timestamp.now() // Timestampにnow()は生えていないので雰囲気です
);

await batch.commit();

このあたりは公式のドキュメントにもありますが、batchの使い方は上記の通りです。
更新、追加、削除したいデータへのDocumentReferenceと追加、更新ならそれを行うデータのMapの組み合わせでリクエストを準備し、batch.commit()でまとめて実行します。

ところで、messagesへの追加をコレクションの新しいドキュメントとなるように行っているのがわかったでしょうか。
データの追加・更新時、messagesをFirestore上においてList型のドキュメントとしているかコレクションとして扱っているかによって変わります。
messagesをコレクションとして扱う利点は、クエリを引くことができるという一点が大きいです。
特定のboolパラメータがtrueになっているものはフィルタするなどの使い方がありますが、本記事の趣旨から外れるため割愛します。

本来messagesChatRoomのもつリストですが、こちらをサブコレクションとしてFirestoreに登録するには、ChatRoomを作成するときにbatchを用いて以下のようなリクエストを行いましょう。

async
final batch = Firestore.instance.batch();
Map<String, dynamic> chatRoom; // setDataするMap

chatRoom = {
 ... // コレクションとして登録するパラメータ以外をMapのkey, valueとして追加
}

final newRoomDocument = Firestore.instance
    .collection("chatRooms")
    .document():

batch.setData(newRoomDocument, chatRoom);

batch.setData(Firestore.instance
    .document("chatRooms/${newRoomDocument.documentId}")
    .collection("messages")
    .document(),
  {
    // ルーム作成時の最初のメッセージのデータを作成する
    ...
  }
);

await batch.commit();

このように、コレクションとして登録したいデータを抜いたMapのsetDataと別で、messagesコレクションへのデータ作成という形でバッチに処理を依頼しましょう。
こういうときのために、Firebaseで使う型定義を行ったクラスにはMapに変換するメソッドを作成するなどすると便利ですね。

おわりに

データの取得と購読、追加と更新までのあれこれを紹介しました。
ここまでのところがわかれば、Flutter+Firestoreを実践で使い始める障害はだいぶ消えるのではないでしょうか。

自分がFlutterとFirestoreを使い始めてから一ヶ月ほどたち、いろいろなドキュメントを探したりしましたが日本語でいい感じのドキュメントがあまりに少ないため(見出しよさそう……ってなってもspeakerdeckのスライドだったりして詳しく見たいとこが見れない)、おれたちが作っていくしかないんやという気持ちです。

みんなでいい感じにドキュメント整備していこうな!!

ありがとうございました。

おまけ

12月初め頃、ようやくFlutterのFirestoreにwhere-inクエリが実装されました :dancer:
FlutterのFirestoreプラグインはまだまだ整備途中な感じなので、コミットチャンスを狙ってみても面白いかなと思います。

23
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?