📟

Firestoreでページネーションを使う

2023/11/20に公開

ページネーションとは?

Webサイトやアプリ内の情報を複数のページに分割して表示する手法です。 一度に全てのデータを表示するのではなく、一部だけを表示し、ユーザーが次のページや前のページに移動することで残りの情報を確認出来るようにします。

こんな感じのものを作りました。ちょっとわかりにくいですが、画面下にローディング表示されます。
https://youtube.com/shorts/typn1nfYbw4?feature=share

🔥Firestore使うときには対策が必要

Firestoreはデータを読み取るのは、1日5万件までは無料で、上限を超えると課金されます😱
なので、SNSのような多くのユーザーが使うアプリや、多くのデータを表示するアプリで課金されないように対策が必要です。

公式の情報だとわかりにくい
https://firebase.google.com/docs/firestore/quotas?hl=ja

AIじゃないと詳しく教えてくれない!

🔥ダミーのデータを作るメソッド

Firestoreは、Supabaseのように、CSVをimportしてダミーのデータを入れることができないので、for文でループして、addメソッドを指定した回数実行するコードを作りました。
ボタンを押すときに、実行すればOK!

// ダミーデータを作成する
  Future<void> createDummyData() async {
  final firestore = FirebaseFirestore.instance;
  final collection = firestore.collection('users');
  // for文で15件のダミーデータを作成する
  for (int i = 0; i < 30; i++) {
    await collection.add({
      'name': 'User $i',
      'createdAt': Timestamp.now(),
    });
  }
}

AppBarのところに、プラスボタンを配置して、ここでダミーのデータを保存するメソッドを実行します。

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            // ダミーデータを作成するボタンを追加する
            onPressed: () async {
              await createDummyData();
            },
            icon: const Icon(Icons.add),
          ),
        ],
        title: const Text('User View'),
      ),

今回実装した機能は、こちらのコードです。綺麗なコードではないですね💦

全体のコード

ページネーションのコード

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

class UserView extends StatefulWidget {
  const UserView({Key? key}) : super(key: key);

  
  _UserViewState createState() => _UserViewState();
}

class _UserViewState extends State<UserView> {
  final ScrollController _scrollController = ScrollController();
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final List<DocumentSnapshot> _users = [];
  bool _isLoading = false;
  DocumentSnapshot? _lastDocument;
  final int _documentLimit = 15;

  
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
    _loadUsers();
  }

  void _loadUsers() async {
    if (_isLoading) return;
    setState(() {
      _isLoading = true;
    });

    QuerySnapshot querySnapshot;
    if (_lastDocument == null) {
      querySnapshot = await _firestore
          .collection('users')
          .orderBy('createdAt', descending: true)
          .limit(_documentLimit)
          .get();
    } else {
      querySnapshot = await _firestore
          .collection('users')
          .orderBy('createdAt', descending: true)
          .startAfterDocument(_lastDocument!)
          .limit(_documentLimit)
          .get();
    }

    if (querySnapshot.docs.isNotEmpty) {
      _lastDocument = querySnapshot.docs.last;
      _users.addAll(querySnapshot.docs);
    }

    setState(() {
      _isLoading = false;
    });
  }

  void _scrollListener() {
    if (_scrollController.position.atEdge &&
        _scrollController.position.pixels != 0 && !_isLoading) {
      _loadUsers();
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User View'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _users.length + (_isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index < _users.length) {
            final user = _users[index];
            return ListTile(
              title: Text(user['name']),
            );
          } else {
            // ローディングインジケーターを表示
            return Padding(
              padding: EdgeInsets.symmetric(vertical: 32.0),
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
        },
      ),
    );
  }

  
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

アプリを実行するコード

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:page_nation_app/user_view.dart';

import '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(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const UserView(),
    );
  }
}

最後に

今回は、ページネーションなるものを作ってみました。
riverpodを使用して状態の管理をおこなおうとしたのですが、うまくいきませんでした!
これは今後の課題ですね。

Discussion