Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Flutter/Flutter Study

[Flutter] Firestore pagination(무한스크롤)

방피터 2022. 11. 13. 23:07

무한 스크롤 같은 거 구현할 때 스크롤이 끝부분에 다달았을 때 DB나 API 서버를 통해서 새로운 데이터를 가져와야 하잖아? 이런 걸 Pagination이라고 해. 보통 리스트 형태의 데이터를 뿌려주는 API 들은 page 옵션이 별도로 있어. 그래서 pagination 구현할 때 그 page 옵션을 사용하면 되지.

영화 DB API, page 옵션이 있다.

 

page 옵션이 없더라도 특정 데이터의 날짜를 기준삼아 정렬하고 읽어온 데이터 갯수를 pagination에 활용할 수도 있어. 뭐 API 서버는 이렇다 치고, Firestore에서도 여러가지 pagination을 위한 기능을 제공하고 있어.👇👇

https://firebase.google.com/docs/firestore/query-data/query-cursors

 

쿼리 커서로 데이터 페이지 매김  |  Firestore  |  Firebase

Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니

firebase.google.com

startAt, startAfter, endAt, endBefore 등의 커서와 limit를 결합해서 pagination에 필요한 데이터를 읽어올 수 있는데, 보통의 경우 이 뿐만 아니라 where과 orderby도 함께 결합되어 사용되지.👇 아래 처럼 말이지 ㅋㅋㅋ

firestore.collection("userInsightCard").withConverter(
    fromFirestore: (snapshot, options) =>
        InsightCardModel.fromJson(snapshot.data()!),
    toFirestore: (value, options) => value.toJson(),
  ).where("author",
          isEqualTo:
              userId != null ? firestore.doc("userData/${userId}") : null)
  .where("chartId", isEqualTo: chartId)
  .orderBy("createdAt", descending: true)
  .startAfterDocument(pageKey)
  .limit(_pageSize)
  .get()

좀 복잡해 보이지? ㅎㅎㅎ 그래서 나는 collection reference + withConverter 까지는 쪼개서 관리해.

//보통 Collection Reference를 사전에 정의해놓고
CollectionReference<InsightCardModel> userInsightCardColRef() {
  return firestore.collection("userInsightCard").withConverter(
        fromFirestore: (snapshot, options) =>
            InsightCardModel.fromJson(snapshot.data()!),
        toFirestore: (value, options) => value.toJson(),
      );
}

//거기에 where query, orderBy 정렬, startAfterDocument 커서, limit 갯수 제한 등을 설정해서 사용
       userInsightCardColRef()
          .where("author",
              isEqualTo:
                  userId != null ? firestore.doc("userData/${userId}") : null)
          .where("chartId", isEqualTo: chartId)
          .orderBy("createdAt", descending: true)
          .startAfterDocument(pageKey)//페이지 커서!!
          .limit(10)
          .get();

어쨋든 위 코드는 flutter에서 firestore document를 10개씩 쿼리해서 읽어오는 코드인데, 이전에 읽어온 쿼리 스냅샷의 마지막 document를 pageKey에 넣으면 연속적인 데이터를 10개씩 가져오겠지.

자 그래서 이걸 flutter에서 사용할 때에는 listview같은 거와 결합해서 사용하게 될거야. scrollController도 설정해서 끝부분에 다달으면 firestore 쿼리 함수 수행해서 listview를 업데이트 하면 되겠지.으흐흐흐흐흐흐흐흐흫 이렇게 하지마 ㅋㅋㅋ

난 그냥 infinite_scroll_pagination을 사용했어.👇

https://pub.dev/packages/infinite_scroll_pagination/example

 

infinite_scroll_pagination | Flutter Package

Lazily load and display pages of items as the user scrolls down your screen.

pub.dev

사용법은 그다지 어렵지 않아. UI부터 보자. 내가 사용중인 코드의 Scaffold 일부를 보자면 👇👇

return Scaffold(
  body: RefreshIndicator(//잡아 땡기면 리프레시!
    //리프레시 함수 등록
    onRefresh: () => Future.sync(
      () => controller.pagingController.refresh(),
    ),
    //본격적인 무한 스크롤뷰
    child: CustomScrollView(
      controller: controller.scrollController,
      slivers: [
        //헤더 삽입햇!
        SliverToBoxAdapter(
          child: header ?? SizedBox(),
        ),
        //본격 무한 스크롤!
        PagedSliverList(
          pagingController: controller.pagingController,
          builderDelegate: PagedChildBuilderDelegate<
              QueryDocumentSnapshot<InsightCardModel>>(
            //리스트로 만들 아이템 빌더!
            itemBuilder: ((context, item, index) {
              return InsightCard(
                  navKey: navKey,
                  cardId: item.id,
                  cardInfo: item.data());
            }),
          ),
        ),
      ],
    ),
  ),

UI쪽은 엄청 간단하게 끝나지? 원래는 PagedListView 위젯을 사용해야 하는데 난 Header를 삽입해야 해서 PagedSliverList를 사용했어. PagedSliverList가 Sliver이기 때문에 Header도 Sliver 이어야 하기 때문에 SliverToBoxAdapter를 사용해서 Sliver를 맞춰줬어. Header같은게 필요없으면 그냥 PagedListView를 사용하도록 하자. 나머지는 코드에 주석을 남겨 놨으니 참고해.

이제 나머지는 컨트롤 부분인데 보통은 stateful widget에 여러 가지를 구현하겠지만 난 GetX를 기본으로 활용해서 GetXController에 코드가 다 있으니 감안해서 보자고 👇

class InsightCardListController extends GetxController {
  InsightCardListController({this.chartId, this.userId});

  //페이징 컨트롤러!! 이 아이가 거의 다 알아서 해줌!
  PagingController<DocumentSnapshot<InsightCardModel?>?,
          QueryDocumentSnapshot<InsightCardModel>> pagingController =
      PagingController(firstPageKey: null);//첫 페이지 키에는 널!(null)

  //스크롤 컨트롤러 물려주고
  ScrollController scrollController = ScrollController();

  var _scrollOffset = 0.0.obs;
  //페이지 사이즈 10개씩 읽어왓!
  final _pageSize = 10;
  double get scrollOffset => _scrollOffset.value;

  String? userId;
  String? chartId;
  
  //읽어온 페이징 데이터 저장할 리스트 변수 (obs)
  final _insightCards = Rx<List<QueryDocumentSnapshot<InsightCardModel>>>([]);

  //getter
  List<QueryDocumentSnapshot<InsightCardModel>> get insightCards =>
      _insightCards.value;

  //pagination이 적용된 firestore query 함수
  void fetchInsightCard(DocumentSnapshot<Object?>? pageKey) async {
    QuerySnapshot<InsightCardModel> loadedInsightCard;
    if (pageKey != null) {//pageKey가 있으면 
      loadedInsightCard = await userInsightCardColRef()
          .where("author",
              isEqualTo:
                  userId != null ? firestore.doc("userData/${userId}") : null)
          .where("chartId", isEqualTo: chartId)
          .orderBy("createdAt", descending: true)
          .startAfterDocument(pageKey)
          .limit(_pageSize)
          .get();
    } else {//pageKey가 null이면 처음부터 읽어왓!
      loadedInsightCard = await userInsightCardColRef()
          .where("author",
              isEqualTo:
                  userId != null ? firestore.doc("userData/${userId}") : null)
          .where("chartId", isEqualTo: chartId)
          .orderBy("createdAt", descending: true)
          .limit(_pageSize)
          .get();
    }
    final isLastPage = loadedInsightCard.docs.length < _pageSize;
    if (isLastPage) {//마지막 페이지인가??
      pagingController.appendLastPage(loadedInsightCard.docs);
    } else {
    //마지막 페이지 아니면 다음 페이지키를 방금 읽어온 데이터의 마지막 document로 설정!
      final nextPageKey = loadedInsightCard.docs.last;
      pagingController.appendPage(loadedInsightCard.docs, nextPageKey);
    }
  }

  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
    scrollController.addListener(() {
      _scrollOffset.value = scrollController.offset;
    });
    //페이징 컨트롤러를 위한 리스너 삽입!
    pagingController.addPageRequestListener((pageKey) {
      fetchInsightCard(pageKey);//페칭!
    });
  }

  @override
  void onClose() {
    // TODO: implement onClose
    super.onClose();
    scrollController.dispose();
    pagingController.dispose();//끝날 때는 쓰레기통에!
  }
}

코드가 쪼끔 길긴한데;;.... ㅋㅋㅋ 아니야 사실 이건 굉장히 짧은 코드야 ㅋ 마우스 스크롤 휘젓지 말고 ㅎㅎ 조금이지만 주석을 달아놨으니! 함 일어바바 ㅋㅋ

GetXController를 약간만 아는 사람이라면 설명이 필요없을 정도로 매우 간결하지. 이 정도 코드양으로 pagination을 구현할 수 있다니 황송해야 할 일이야 ㅋㅋㅋㅋ 뭐 그냥 그렇다고 ㅋㅋ 더 궁금한 게 있으면 댓글 달던지 아님 메일 쏘던지.. ㅎㅎㅎ 화이팅!

Pagination!

 

반응형