🧛

[Flutter]Firestoreの取得制限、ページング機能のある掲示板を作る

7 min read

初めに

こんばんは、えんでばーです。

掲示板やチャット画面なんかでFirestoreを使う際取得数に制限をかけ
Listの一番下までスクロールしたら新たなデータを取ってくる
ってような機能を作ってみたので、僕が実装した方法について話そうかなと思います。

状態管理をGetXで書いますが、本質は変わらないと思うのでふーんぐらい見てて貰えるとありがたいです。

Pine慶應大学限定SNSに使っています。↓

https://apps.apple.com/jp/app/pine-パイン-慶應義塾大生限定sns/id1590550932

GetXの簡単な説明

  • RxListとかRxStringとか型の前にRxが付いたやつが出てくると思いますが、こちらは変数をリアクティブにしています。
  • onInitはstfのinitState
  • onCloseはstfのdispose

知っておきたい事

Firestoreで取得制限をかけるのに必要なコードは

FirebaseFirestore.instance.collection('post').limit(15);

.limit()でドキュメント数に制限をかけてとってこれます。
上記だと15個のドキュメントをとってこれます。

そしてページング機能で一番大事なコードが

FirebaseFirestore.instance.collection('post').limit(15).startAfterDocument(documentsnapshot).get()

.startAfterDocument()は引数に最後のDocumentSnapshotを入れて上げる事でそれ以降のドキュメントを取得できます。

この二つを組み合わせる事で15個取ってきて、さらに次の15個を取ってくるという様な事が可能になり、コストも下げる事ができます。

実装する前に考える事

最初はデータを取ってきていない為,startAfterDocument()の引数に入れる事ができないので、
分岐させてあげる必要があります。

実装

それでは実装をしていきます。
PineはオープンチャットSNSです。

1.  とりあえずModel

class PostModel {
  final Timestamp createdAt;
  final String genre;
  final String title;
  //多いので省略
  PostModel({
    required this.createdAt,
    required this.genre,
    required this.title,
    //多いので省略
  });

  factory PostModel.fromFirestore(DocumentSnapshot snapshot) {
    return PostModel(      
      createdAt: snapshot["createdAt"],
      genre: snapshot["genre"],
      title: snapshot["title"],
      //多いので省略
    );
  }
}

2. DocumentSnapshot型の空配列を作る。

3. 取得制限したい数を宣言しとく。

4. 必要そうなやつ諸々

class LatestController extends GetxController {
  RxList<DocumentSnapshot> _latestList = RxList<DocumentSnapshot>();
  //これ配列の最後のDocumentSnapshotをstartAfterDocument()にぶち込むよ
  //GetX使ってない人場合またはGetXのRx使わない場合
  //List<DocumentSnapshot> _latestList = [];
  final int limit = 15;
  //取得制限用
  final String myId = FirebaseAuth.instance.currentUser!.uid;
  //とりあえずuid取っとく。
  final ScrollController scrollController = ScrollController();
  //ListView.builderのコントローラーも作成しとく。後で必要になるよ
  RxList<PostModel> get genreList => _latestList
      .map((snap) {
        return PostModel.fromFirestore(snap);
  }).toList().obs;
  //View側で使いやすくする為にDocumentSnapshot型からPostModelに変換しています。
  //GetX使ってない人場合またはGetXのRx使わない場合
  //PostModel get genreList => _latestList.map((snap) {
  //      return PostModel.fromFirestore(snap);
  // }).toList()
 }

5. 取得制限してデータを取ってくる

class LatestController extends GetxController {
~~~~~~~~~~~~~~~~~~

  void onInit()async {
    super.onInit();
    await fetchNextData(
      _latestList.isNotEmpty ? _latestList.last : null,
    );
 }
 Future<void> fetchNextData(DocumentSnapshot? startAfter) async   {
    final userRef = FirebaseFirestore.instance
        .collection('post')
        .orderBy("lastMessageAt", descending: true)
        .limit(limit);
    if (startAfter == null) {
      userRef.get().then((QuerySnapshot querySnapshot) async {
        querySnapshot.docs.forEach((doc) {
          if (!PostModel.fromFirestore(doc).block.contains(myId)) {
            _latestList.add(doc);
          }
        });
      });
    } else {
      userRef
          .startAfterDocument(startAfter)
          .get()
          .then((QuerySnapshot querySnapshot) async {
        querySnapshot.docs.forEach((doc) {
          if (!PostModel.fromFirestore(doc).block.contains(myId)) {
            _latestList.add(doc);
          }
        });
      });
    }
  }
~~~~~~~~~~~~~~~~~~
}

5の説明

まずはonInitの中でデータを取ってくる関数を呼び出します。
4で宣言した_latestListは最初はデータを持っていない為、fetchNextData()の引数にnullを送ります。

await fetchNextData(
  _latestList.isNotEmpty ? _latestList.last : null,
);

引数がnullの場合は、シンプルに15件取得しlatestListに入れます。

FirebaseFirestore.instance
        .collection('post')
        .orderBy("lastMessageAt", descending: true)//最新順
        .limit(limit)//ここで取得制限
	.get();

余談
userRef.get().then((QuerySnapshot querySnapshot) async {
  querySnapshot.docs.forEach((doc) {
    if (!PostModel.fromFirestore(doc).block.contains(myId)) {
         _latestList.add(doc);
     }
});

の部分はAppleのリジェクト回避の為にブロック機能を作っていますが、本記事とは関係ないので
説明はしません。

ブロック機能をつけない場合は

userRef.get().then((QuerySnapshot querySnapshot) async {
    _latestList.addAll(querySnapshot.docs);   
});

って感じでforeachとか書かずに書けるのでめっちゃスッキリしていいんですけどね。。笑


6. 一番下までスクロールしたら次のデータを取ってくる

上で取ってきたデータをListView.builderで並べ一番下まで行った時、
次のデータを取ってくるって事をやらなければなりません。
上のコードに追加しView側も書くことがあるので注意です。

まずはView側のListViewにコントローラーを追加

class LatestView extends StatelessWidget {
  final LatestController _latestController = Get.put(LatestController());
  //自粛
  ~~~~~~~~~~~~~~~~~~
  ListView.builder(
    controller: _latestController.scrollController,
    //4で宣言したscrollControllerに追加する。↑
    //こいつでListViewのスクロール状況がわかる。
    itemCount: _latestController.genreList.length,
    itemBuilder: (BuildContext context, int index) {
      return //自粛
    }
  )
  ~~~~~~~~~~~~~~~~~~
}

次にコントローラーにリスナーくっつける

class LatestController extends GetxController {
~~~~~~~~~~

  void onInit() {
    super.onInit();
    scrollController.addListener(scrollListener);リスナーをくっつける
    Future(() async {
      await fetchNextData(
        latestList.isNotEmpty ? latestList.last : null,
      );
    });
  }

 void onClose() {
   scrollController.dispose();
   //スクロールコントローラのディタッチ
   super.onClose();
 }
  
void scrollListener() async {
    if (scrollController.hasClients) {
      if (scrollController.offset >=
              scrollController.position.maxScrollExtent &&
          !scrollController.position.outOfRange) {
	  //これでスクロール最後まで行ったらしたの関数を呼べる!!
        await fetchNextData(latestList.last);
	//これを読んだとき一回データを取りに行っているのでlatestListの最後の
	//documentsnapshotを引数に送ってあげる。
      }
    }
  }
 
~~~~~~~~~~
}

これによりfetchNextData()の引数がnullではないので

 userRef
   .startAfterDocument(startAfter)
   .get()
   .then((QuerySnapshot querySnapshot) async {
  querySnapshot.docs.forEach((doc) {
    if (!PostModel.fromFirestore(doc).block.contains(myId)) {
       _latestList.add(doc);
      }
     });
  });

こっち側が呼ばれ次のデータを取得する事ができる。
終わり。


画面のリフレッシュがしたい方は

RefreshIndicatorなどで括り、
こんな感じでリフレッシュするのがいいんじゃないでしょうか?

Future<void> reflesGenreList() async {
    await clearList();
    await fetchNextData(
      latestList.isNotEmpty ? latestList.last : null,
    );
 }

Future<void> clearList() async {
  latestList.clear();
  genreList.clear();
}

最後に

いかがだったでしょうか?
わからない事があったらtwitterのDMに連絡くだされば回答します。
記事の間違いなどありましたら、こっそり教えて貰えると助かります。

この記事がいいと思ったらいいねを押して下さい。めちゃめちゃ励みになります。

もしよければtwitterのフォローもお願いします。
仲良くなりましょう!

https://twitter.com/endeverva

Discussion

ログインするとコメントできます