🦏

firebase_ui_firestore

2023/03/25に公開

今後人気が出るかもしれない?

アンドレアさんが、Twitterでご紹介していた、面白そうなパッケージがあったので、使ってみました。
Firestoreの従量課金対策に使えるそうです?
アンドレアさんのブログ
https://codewithandrea.com/articles/firestore-pagination-list-view/
今回使用したパッケージ
https://pub.dev/packages/firebase_ui_firestore

翻訳

Cloud Firestoreで作業する場合、ページネーションは大きなデータセットを扱う際に特に有効です。Firestoreのコレクションには何千ものドキュメントが含まれることがあり、それらをすべて一度に取得すると、パフォーマンスに大きな問題が生じ、帯域幅が過度に消費されることがあります。

また、Firestoreは従量制の料金モデルで運営されているため、無料枠(10万文書あたり0.06ドルの課金)を超える文書を読み込むたびに、料金を支払う必要があります。

さらに、一度に表示できる項目は限られているため(特にモバイルデバイス)、ページネーションを実装して必要なデータだけを取得するのが賢明です。

しかし、Cloud Firestoreからデータを読み込む際に、Flutterアプリでページネーションを有効にするにはどうすればよいでしょうか。

公式ドキュメントを検索してみると、クエリカーソルでデータのページ送りができることがわかります。

しかし、この方法をとると、やはりページネーションのコードをすべて手書きで書く必要があります:

さいしょのとりだし
スタートカーソル、エンドカーソルの管理
スクロールダウンしたときの各ページの取得
ページネーションを処理してくれる、使いやすいドロップイン・ウィジェットがあれば便利だと思いませんか?

幸いなことに、Firebase UI for Firestore パッケージには、まさにそれを実現する FirestoreListView ウィジェットが用意されています。


まず使ってみた

必要なパッケージを追加して、Firestoreにダミーのデータを追加してください。私は、ドキュメントを12個ぐらい用意しました。

flutter pub add firebase_core
flutter pub add cloud_firestore
flutter pub add firebase_ui_firestore



以下のコードを追加するだけで、StreamBuilderと同じ動作をしている機能を使用できます。

final usersQuery = FirebaseFirestore.instance.collection('user').orderBy('name');

FirestoreListView<Map<String, dynamic>>(
  query: usersQuery,
  itemBuilder: (context, snapshot) {
    Map<String, dynamic> user = snapshot.data();

    return Text('User name is ${user['name']}');
  },
);

公式によると、このような役割を果たしてくれるそうです。

無限大スクロール
無限スクロールとは、ユーザーがアプリケーションをスクロールしている間、データベースからより多くのデータを継続的に読み込むという概念です。アプリケーションのレンダリングを高速化し、ユーザーが見ることのないデータのためのネットワークのオーバーヘッドを削減することができるため、大規模なデータセットを持つ場合に便利です。

Firebase UI for Firestoreは、FirestoreListViewウィジェットでFirestoreデータベースを使用した無限スクロールを実装する便利な方法を提供します。

最低限、ウィジェットはFirestoreクエリとアイテムビルダーを受け付けます。ユーザーがリストをスクロールダウン(または横断)すると、より多くのデータがデータベースから自動的にフェッチされます(順序などのクエリ条件を尊重しながら)。

まず、クエリを作成し、アイテムビルダーを提供します。この例では、usersコレクションからユーザーのリストを表示することにします:

コード全体
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_firestore/firebase_ui_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tutorial/firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirestoreExample(),
    );
  }
}

class FirestoreExample extends StatelessWidget {
  const FirestoreExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final usersQuery =
        FirebaseFirestore.instance.collection('user').orderBy('name');

    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase UI'),
      ),
      body: FirestoreListView<Map<String, dynamic>>(
        query: usersQuery,
        itemBuilder: (context, snapshot) {
          Map<String, dynamic> user = snapshot.data();

          return Text('User name is ${user['name']}');
        },
      ),
    );
  }
}

スクリーンショット


横向きにスクロールもできるらしい?

FirestoreListViewウィジェットはFlutter独自のListViewウィジェットの上に構築されており、オプションで提供できる同じパラメータを受け取ります。例えば、スクロール方向を水平に変更する場合:

FirestoreListView<Map<String, dynamic>>(
  scrollDirection: Axis.horizontal,
  // ...
);
変更後のコード
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_firestore/firebase_ui_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tutorial/firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirestoreExample(),
    );
  }
}

class FirestoreExample extends StatelessWidget {
  const FirestoreExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final usersQuery =
        FirebaseFirestore.instance.collection('user').orderBy('name');

    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase UI'),
      ),
      body: FirestoreListView<Map<String, dynamic>>(
        scrollDirection: Axis.horizontal,// 追加すると水平にスクロールする
        query: usersQuery,
        itemBuilder: (context, snapshot) {
          Map<String, dynamic> user = snapshot.data();

          return Text('User name is ${user['name']}');
        },
      ),
    );
  }
}

表示するアイテム数の数を指定するのを試してみたのですが、変化がなかったので、今回こちらは手をつけないでおきます。

WithConverterを使用できるらしい?

一般に、ネットワークのオーバーヘッドを減らすために、この値をできるだけ小さくしておくとよいでしょう。個々のアイテムの高さ(または幅)が大きい場合は、ページサイズを小さくすることをお勧めします。

タイプ分けされた回答の使用
cloud_firestoreプラグインでは、withConverter APIを使用して、データベースから受け取ったレスポンスをタイプすることができます。詳しくは、ドキュメントをご覧ください。

FirestoreListViewは、箱から出してすぐにこれに対応します。例えば、変換されたクエリをウィジェットに提供するだけです:

モデルを定義したファイルを別に作成して、動作を試してみましょう。user_model.dartを作成して、WithConverterで使用するユーザーモデルを作成します。
その後に、main.dartを編集します。いい感じのコードが書けなかったので、皆さんもいろいろ試してみて、いけてるコードを書いてみてください。

user_model.dart
class User {
  User({required this.name, required this.age});

  User.fromJson(Map<String, Object?> json)
      : this(
          name: json['name']! as String,
          age: json['age']! as int,
        );

  final String name;
  final int age;

  Map<String, Object?> toJson() {
    return {
      'name': name,
      'age': age,
    };
  }
}
WithConverterを使用したコード
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_firestore/firebase_ui_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tutorial/firebase_options.dart';
import 'package:flutter_tutorial/user_model.dart';

late CollectionReference<User> collection;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  final collectionRef = FirebaseFirestore.instance.collection('user');
  collection = collectionRef.withConverter<User>(
    fromFirestore: (snapshot, _) => User.fromJson(snapshot.data()!),
    toFirestore: (user, _) => user.toJson(),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirestoreExample(),
    );
  }
}

class FirestoreExample extends StatelessWidget {
  const FirestoreExample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase UI'),
      ),
      body: FirestoreListView<User>(
        query: collection,
        itemBuilder: (context, snapshot) {
          // Data is now typed!
          User user = snapshot.data();

          return Column(
            children: [
              const SizedBox(height: 20.0),
              Text(user.name),
              Text(user.age.toString()),
            ],
          );
        },
      ),
    );
  }
}

スクリーンショット

最後に

使ってみた感想ですが、短いコードを書くだけで、縦にも横にもスクロールするStreamBuilderのような機能を使えるのだろうなと思いました。
読み取りコストを下げることもできるらしいので、うまく使えば課金対策になるかもしれないですね。
でもこの手のパッケージってバージョンアップした時に、Firebaseのエラーで詰まることが多いので、アップデートすれば治りますけど、サポートが切れたら使えなくなるので、代替手段を用意しておく必要がありますね。

Discussion