Flutter × Firebaseでページネーションを実装する
FlutterとFirebaseでアプリを作っていて、何かのリスト表示をしたい時、アイテム数が多いとFirebaseの読み取り量が増えてしまいます。
例えば、タイムラインとか、ユーザー検索結果の表示とか。
Firebaseは従量課金制なので、コストに大きく影響します。
特に、私のように個人開発でアプリをリリースしている人にとっては、コスパを重視した設計は結構重要な視点かなと思います。
ということで今回は、リストを一気に読み込まず、例えば10個読み込んで、リストの最下部にボタンを配置して、押したらさらに読み込む、という実装をしたいと思います。いわゆるページネーションです。
前提条件
- FlutterとFirestoreの接続が終わっている
- 状態管理がちょっとわかる(今回は、riverpodを使いますが、状態管理にフォーカスしていないので、詳しくなくて大丈夫です)
- MVVMモデルをちょっと知っている
サンプルアプリの概要
firestoreに名前のドキュメントが30個くらい入っていて、それをテキストボタンを押すたびに10個ずつ取ってくる、というシンプルなものです。
動画で見てみるとこんな感じ
ざっくりイメージ図
行うことのイメージとしては、この図になります。
見にくくてすいません。ぜひ拡大してください。
①db.dart経由で、Firestoreからデータを取ってくる
②&③db.dart => viewModel.dart でデータを受けて、2つの変数に値を入れ込みます。
・名前リスト1(firestoreからデータを取ってくるたびに中身が最新になる)
・名前リスト2(firestoreからデータを取ってくるたびにリスト1を経由して、どんどん溜まっていく)
④main.dartで画面表示をしています。そこに表示される名前一覧は、上記の名前リスト2を見ています。
⑤もっと読み込む を押すと、①〜③が再度実行されて、表示が増えていきます。
⑥そして、⑤までを繰り返してきて、いよいよデータの最後になったら、名前リスト1が空になります。firestoreから、空の配列が帰ってきているということ。そしたら、「結果は以上です」という表記が出ることになります。
viewModel.dartで名前を格納する変数を2つ用意しているのは、⑥の動作のためです。
実際のコード
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(viewModel);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text("ページネーション"),
actions: [
Row(
children: [
TextButton(onPressed: () => model.getNames(ref), child: const Text("取得"))
],
),
]),
body: model.stackedNameList.isEmpty //ここで見ているのは、イメージ図の「名前リスト2」です
? const Center(child: Text("まだ何もありません"))
: Padding(
padding: const EdgeInsets.all(10.0),
child: SingleChildScrollView(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: model.stackedNameList.length,
itemBuilder: (context, int index) {
final name = model.stackedNameList[index];
return Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(name),
));
}),
model.currentNameList.isEmpty//ここで見ているのは、イメージ図の「名前リスト1」です
? const Center(child: Text("結果は以上です"))
: TextButton(
onPressed: () => model.getNamesNext(ref),
child: const Text("もっと読み込む"))
],
),
),
),
);
}
}
1.右上の「取得」を押すと、Firebaseからデータを取得します(10件)。
2.「もっと読み込む」ボタンを押すと、さらにデータを取得します。
新たに表示するデータがもう無い場合は、「結果は以上です」の表示に切り替えます。
viewModel.dart
final viewModel = ChangeNotifierProvider((ref) => ViewModel());
class ViewModel with ChangeNotifier {
List<String> stackedNameList = [];
List<String> currentNameList = [];
Future<void> getNames (WidgetRef ref) async {
stackedNameList = [];
currentNameList = await ref.read(dbManager).getNames();
for (var element in currentNameList) {
stackedNameList.add(element);
}
notifyListeners();
}
Future<void> getNamesNext (WidgetRef ref) async {
currentNameList = await ref.read(dbManager).getNamesNext();
for (var element in currentNameList) {
stackedNameList.add(element);
}
notifyListeners();
}
}
1.main.dartの「取得」ボタンを押すと、getNames メソッドが走って、dbManagerに処理を外注します。
2.さらに、「もっと読み込む」ボタンを押すと、getNamesNext メソッドが走って、dbManagerに外注します。
3.上記2つのメソッドで,dbManager経由でFirestoreから名前リストを取ってきて,
currentNameListと、stackedNameListに結果を格納しておきます。
4.取得できたら、 notifyListeners(); で、main.dartのMyHomePageクラスのbuildメソッドを再ビルドして、名前リストを表示させます。
dbManager.dart
final dbManager = ChangeNotifierProvider((ref) => DbManager());
class DbManager with ChangeNotifier {
final db = FirebaseFirestore.instance;
DocumentSnapshot? lastDoc;
Future<List<String>> getNames () async{
var result = <String>[];
final query = await db.collection("users").limit(10).get();
if(query.docs.isNotEmpty){
for (var element in query.docs) {
result.add(element.data()["name"]);
}
lastDoc = query.docs.last;
}
return result;
}
Future<List<String>> getNamesNext () async{
var result = <String>[];
final query = await db.collection("users").startAfterDocument(lastDoc!).limit(10).get();
if(query.docs.isNotEmpty){
for (var element in query.docs) {
result.add(element.data()["name"]);
}
lastDoc = query.docs.last;
}
return result;
}
}
dbManagerでは、Firestoreからのデータ取得を行います。
ここでのポイントは、
1. limit(10) で、ドキュメントの取得数を10個に制限しています。
2. DocumentSnapshot? lastDoc; を変数として設定。getNames()、getNamesNext()メソッド内で最後に取得したドキュメントを、lastDocに格納しておきます。
lastDoc = query.docs.last;
↑↑この部分です。
3.そうしたら、getNamesNext()メソッド(<= もっと読み込む、を押したら走るメソッド)の、
startAfterDocument(lastDoc!)
この部分で、前回最後に読み込んだドキュメントの次のドキュメントから、読み込みを開始してくれます。
こんな流れで実装すると、ページネーション機能が作成できるはずです。
↓gitはこちら↓
もっといいやり方があったら、ぜひ教えていただけるとありがたいです!
ご指摘、ご質問などありましたらよろしくお願いします〜!
Discussion