15

hello i am trying to use pagination in my list. and the list data is coming from firebase. i am not sure how to paginate data using stream builder. now i am doing the pagination by calling getdocuments in the didChangeDependencies() function. but after adding new data the list is not updated. if anyone can help then it will be great for me. here is what i am doing now..

 didChangeDependencies() {
    super.didChangeDependencies();

   getProducts();

    _scrollController.addListener(() {
      double maxScroll = _scrollController.position.maxScrollExtent;
      double currentScroll = _scrollController.position.pixels;
      double delta = MediaQuery.of(context).size.height * 0.20;
      if (maxScroll - currentScroll <= delta) {
        getProducts();
      }
    });
  }

  getProducts() async {
    if (!hasMore) {
      return;
    }
    if (isLoading) {
      return;
    }
    setState(() {
      isLoading = true;
    });
    QuerySnapshot querySnapshot;
    if (lastDocument == null) {
      querySnapshot = await firestore
          .collection('products')
          .limit(documentLimit)
          .orderBy('timestamp', descending: true)
          .getDocuments();
    } else {
      querySnapshot = await firestore
          .collection('products')
          .startAfterDocument(lastDocument)
          .limit(documentLimit)
          .orderBy('timestamp', descending: true)
          .getDocuments();
    }
    if (querySnapshot.documents.length < documentLimit) {
      hasMore = false;
    }
    if (querySnapshot.documents.isNotEmpty) {
      lastDocument =
          querySnapshot.documents[querySnapshot.documents.length - 1];
      products.addAll(querySnapshot.documents);
      setState(() {
        isLoading = false;
      });
    }
  }
Sourav Das
  • 286
  • 1
  • 4
  • 19

4 Answers4

11

Take a try with below code:

class ProductList extends StatefulWidget {
  @override
  _ProductListState createState() => _ProductListState();
}

class _ProductListState extends State<ProductList> {
  StreamController<List<DocumentSnapshot>> _streamController =
  StreamController<List<DocumentSnapshot>>();
  List<DocumentSnapshot> _products = [];

  bool _isRequesting = false;
  bool _isFinish = false;

  void onChangeData(List<DocumentChange> documentChanges) {
    var isChange = false;
    documentChanges.forEach((productChange) {
      if (productChange.type == DocumentChangeType.removed) {
        _products.removeWhere((product) {
          return productChange.document.documentID == product.documentID;
        });
        isChange = true;
      } else {

        if (productChange.type == DocumentChangeType.modified) {
          int indexWhere = _products.indexWhere((product) {
            return productChange.document.documentID == product.documentID;
          });

          if (indexWhere >= 0) {
            _products[indexWhere] = productChange.document;
          }
          isChange = true;
        }
      }
    });

    if(isChange) {
      _streamController.add(_products);
    }
  }

  @override
  void initState() {
    Firestore.instance
        .collection('products')
        .snapshots()
        .listen((data) => onChangeData(data.documentChanges));

    requestNextPage();
    super.initState();
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (scrollInfo.metrics.maxScrollExtent == scrollInfo.metrics.pixels) {
            requestNextPage();
          }
          return true;
        },
        child: StreamBuilder<List<DocumentSnapshot>>(
          stream: _streamController.stream,
          builder: (BuildContext context,
              AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
            if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.waiting:
                return new Text('Loading...');
              default:
                log("Items: " + snapshot.data.length.toString());
                return ListView.separated(
                  separatorBuilder: (context, index) => Divider(
                    color: Colors.black,
                  ),
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 32),
                    child: new ListTile(
                      title: new Text(snapshot.data[index]['name']),
                      subtitle: new Text(snapshot.data[index]['description']),
                    ),
                  ),
                );
            }
          },
        ));
  }

  void requestNextPage() async {
    if (!_isRequesting && !_isFinish) {
      QuerySnapshot querySnapshot;
      _isRequesting = true;
      if (_products.isEmpty) {
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .limit(5)
            .getDocuments();
      } else {
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .startAfterDocument(_products[_products.length - 1])
            .limit(5)
            .getDocuments();
      }

      if (querySnapshot != null) {
        int oldSize = _products.length;
        _products.addAll(querySnapshot.documents);
        int newSize = _products.length;
        if (oldSize != newSize) {
          _streamController.add(_products);
        } else {
          _isFinish = true;
        }
      }
      _isRequesting = false;
    }
  }
}

JSON for product item:

{
  "index": 1,
  "name": "Pork",
  "description": "Thịt heo/lợn"
}

Link Github example: https://github.com/simplesoft-duongdt3/flutter_firestore_paging

duongdt3
  • 1,648
  • 11
  • 16
  • 1
    Can you please provide the code for pagination using stream of querysnapshot....not list of documents snapshot – Sourav Das Dec 10 '19 at 12:20
  • @SouravDas I have add example Widget for u. Please check it. – duongdt3 Dec 11 '19 at 03:42
  • 1
    i want pagination with real time update...but your code only works for pagination..but real time update is not working – Sourav Das Dec 18 '19 at 21:09
  • If you want to realtime update, get new items when load more, please remove `_isFinish` variable. It'll fit your goal now. – duongdt3 Dec 19 '19 at 01:20
  • @SouravDas I edited my example code for realtime update, it works, now. – duongdt3 Dec 19 '19 at 01:51
  • the real time update is not working...have a look at the video...https://drive.google.com/file/d/1ROtK2frChUxbx3-aGrjDEw_5mkM_-Zf1/view?usp=sharing – Sourav Das Dec 19 '19 at 08:06
  • @SouravDas I mean real-time update is when adds new product -> can load more. If you want to update product real-time -> listen to changes -> update List item. – duongdt3 Dec 19 '19 at 08:42
  • can you please provide solution of my problem – Sourav Das Dec 19 '19 at 08:45
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/204538/discussion-between-sourav-das-and-duongdt3). – Sourav Das Dec 19 '19 at 08:55
  • 3
    I am following this and got my results. But, wouldn't this initially give a thhousands of read to my collections of products in the initstate ? If so, isn't it worth to listen to their changes in requestNextPage method as we read them ? – rohan koshti Jun 26 '20 at 19:50
  • @rohankoshti I think what you say is correct, it works very well, but I also think that the amount of reading is excessive. I just tested it in a chat app and consumed thousands of readings, even doing only a few tests. – Pablo Crucitta Jun 30 '20 at 16:49
  • 1
    @PabloCrucitta - thanks.. although I have updated it so that it consumes lesser reads...before .listen((data) => onChangeData(data.documentChanges)); add .limit(whatever_pagination_you_have)... so it would reducce your reads.. This would listen to only the snapshots that have been added and not to all snapshots unnecessary. – rohan koshti Jul 01 '20 at 03:47
  • Import import 'dart:async'; for StreamController – Krishna Shetty Oct 09 '20 at 03:57
  • To get real time update I added a boolean like this https://stackoverflow.com/a/76356675/14207423 – Ilyas Arafath May 29 '23 at 10:47
  • @duongdt3 I took your code and updated it for the current Flutter version. It is working fine, but I also need to handle the case `DocumentChangeType.added`. I tried simply calling `_products.add(productChange.doc)`. But now I get a weird behavior where on init, each item appears twice in the list. Any idea why? – Chris Jun 28 '23 at 21:52
  • I just tested it again, and as @rohankoshti already pointed out: This code result in a larger amount of read because it initially reads all documents in the collection. – Chris Jun 29 '23 at 08:54
5

I did something similar with chats instead of products.

See if this code is helpful:

class _MessagesState extends State<Messages> {
  ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.offset >=
              (_scrollController.position.maxScrollExtent) &&
          !_scrollController.position.outOfRange) {
        _getChats();
      }
    });
  }

  final StreamController<List<DocumentSnapshot>> _chatController =
      StreamController<List<DocumentSnapshot>>.broadcast();

  List<List<DocumentSnapshot>> _allPagedResults = [<DocumentSnapshot>[]];

  static const int chatLimit = 10;
  DocumentSnapshot? _lastDocument;
  bool _hasMoreData = true;

  Stream<List<DocumentSnapshot>> listenToChatsRealTime() {
    _getChats();
    return _chatController.stream;
  }

  void _getChats() {
    final CollectionReference _chatCollectionReference = FirebaseFirestore
        .instance
        .collection("ChatRoom")
        .doc(widget.chatRoomId)
        .collection("channel");
    var pagechatQuery = _chatCollectionReference
        .orderBy('createdAt', descending: true)
        .limit(chatLimit);

    if (_lastDocument != null) {
      pagechatQuery = pagechatQuery.startAfterDocument(_lastDocument!);
    }

    if (!_hasMoreData) return;

    var currentRequestIndex = _allPagedResults.length;
    pagechatQuery.snapshots().listen(
      (snapshot) {
        if (snapshot.docs.isNotEmpty) {
          var generalChats = snapshot.docs.toList();

          var pageExists = currentRequestIndex < _allPagedResults.length;

          if (pageExists) {
            _allPagedResults[currentRequestIndex] = generalChats;
          } else {
            _allPagedResults.add(generalChats);
          }

          var allChats = _allPagedResults.fold<List<DocumentSnapshot>>(
              <DocumentSnapshot>[],
              (initialValue, pageItems) => initialValue..addAll(pageItems));

          _chatController.add(allChats);

          if (currentRequestIndex == _allPagedResults.length - 1) {
            _lastDocument = snapshot.docs.last;
          }

          _hasMoreData = generalChats.length == chatLimit;
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: StreamBuilder<List<DocumentSnapshot>>(
          stream: listenToChatsRealTime(),
          builder: (ctx, chatSnapshot) {
            if (chatSnapshot.connectionState == ConnectionState.waiting ||
                chatSnapshot.connectionState == ConnectionState.none) {
              return chatSnapshot.hasData
                  ? Center(
                      child: CircularProgressIndicator(),
                    )
                  : Center(
                      child: Text("Start a Conversation."),
                    );
            } else {
              if (chatSnapshot.hasData) {
                final chatDocs = chatSnapshot.data!;
                final user = Provider.of<User?>(context);
                return ListView.builder(
                  controller: _scrollController,
                  reverse: true,
                  itemBuilder: (ctx, i) {
                    Map chatData = chatDocs[i].data() as Map;
                    return MessageBubble(
                        username: chatData['username'],
                        message: chatData['text'],
                        isMe: chatData['senderId'] == user!.uid,
                        key: ValueKey(chatDocs[i].id));
                  },
                  itemCount: chatDocs.length,
                );
              } else {
                return CircularProgressIndicator();
              }
            }
          }),
    );
  }
}

I referred to this answer: Pagination in Flutter with Firebase Realtime Database

YAPS
  • 511
  • 6
  • 7
1

@sourav-das, please have look this code

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Firestore firestore = Firestore.instance;
  List<DocumentSnapshot> products = [];
  bool isLoading = false;
  bool hasMore = true;
  int documentLimit = 10;
  DocumentSnapshot lastDocument;
  ScrollController _scrollController = ScrollController();

  StreamController<List<DocumentSnapshot>> _controller =
      StreamController<List<DocumentSnapshot>>();

  Stream<List<DocumentSnapshot>> get _streamController => _controller.stream;

  @override
  void initState() {
    super.initState();
    getProducts();
    _scrollController.addListener(() {
      double maxScroll = _scrollController.position.maxScrollExtent;
      double currentScroll = _scrollController.position.pixels;
      double delta = MediaQuery.of(context).size.height * 0.20;
      if (maxScroll - currentScroll <= delta) {
        getProducts();
      }
    });
  }

  getProducts() async {
    if (!hasMore) {
      print('No More Products');
      return;
    }
    if (isLoading) {
      return;
    }
    setState(() {
      isLoading = true;
    });
    QuerySnapshot querySnapshot;
    if (lastDocument == null) {
      querySnapshot = await firestore
          .collection('products')
          .orderBy('name')
          .limit(documentLimit)
          .getDocuments();
    } else {
      querySnapshot = await firestore
          .collection('products')
          .orderBy('name')
          .startAfterDocument(lastDocument)
          .limit(documentLimit)
          .getDocuments();
      print(1);
    }
    if (querySnapshot.documents.length < documentLimit) {
      hasMore = false;
    }

    lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];

    products.addAll(querySnapshot.documents);
    _controller.sink.add(products);

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Pagination with Firestore'),
      ),
      body: Column(children: [
        Expanded(
          child: StreamBuilder<List<DocumentSnapshot>>(
            stream: _streamController,
            builder: (sContext, snapshot) {
              if (snapshot.hasData && snapshot.data.length > 0) {
                return ListView.builder(
                  controller: _scrollController,
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      contentPadding: EdgeInsets.all(5),
                      title: Text(snapshot.data[index].data['name']),
                      subtitle: Text(snapshot.data[index].data['short_desc']),
                    );
                  },
                );
              } else {
                return Center(
                  child: Text('No Data...'),
                );
              }
            },
          ),
        ),
        isLoading
            ? Container(
                width: MediaQuery.of(context).size.width,
                padding: EdgeInsets.all(5),
                color: Colors.yellowAccent,
                child: Text(
                  'Loading',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              )
            : Container()
      ]),
    );
  }
}
Maulik Sinroja
  • 191
  • 1
  • 6
  • 17
  • 1
    You are doing same as I did..I am asking for how can I use streambuilder for pagination. So I can I can listen to changes of data.. – Sourav Das Dec 10 '19 at 12:16
  • @SouravDas Please check I have edited the code with StreamBuilder. – Maulik Sinroja Dec 10 '19 at 12:32
  • when i scrolls it removes the previous item of list replace them with new item. like if there are 20 items in total and i added limit of 10 then after scrolling the first 10, it removes them and add the new 10 item...it's not the correct way – Sourav Das Dec 10 '19 at 13:26
  • You assign value to the variable products but where did you use it.... streambuilder is same as before... please provide correct solution if you can. Thank you – Sourav Das Dec 10 '19 at 16:52
  • For me its working fine in my code, please check your code line by line. I think you did something wrong in your code. – Maulik Sinroja Dec 10 '19 at 17:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/203995/discussion-between-sourav-das-and-maulik-gajjar). – Sourav Das Dec 10 '19 at 18:13
  • 1
    @MaulikGajjar: Did either of you figure out this problem? The code above is working for me, but I want to instantly update this once a new document is added (in case of a chat app) and this code is not doing that. – PJQuakJag Feb 12 '20 at 16:23
  • @PJQuakJag yes exactly.... pagination works fine but when I add data it doesn't give that data at that time. any solution?? – Lakshydeep Vikram Sah Jan 24 '23 at 15:34
1

@duongdt3 answer is correct. To reflect realtime update in the list I added a boolean condition

 class ProductList extends StatefulWidget {
          @override
          _ProductListState createState() => _ProductListState();
        }
    
    class _ProductListState extends State<ProductList> {
      StreamController<List<DocumentSnapshot>> _streamController =
      StreamController<List<DocumentSnapshot>>();
      List<DocumentSnapshot> _products = [];
    
      bool _isRequesting = false;
      bool _isFinish = false;
      bool _isFirst = false;

    
      void onChangeData(List<DocumentChange> documentChanges) {
        var isChange = false;
        documentChanges.forEach((productChange) {
         if (productChange.type == DocumentChangeType.removed) {
            _products.removeWhere((product) {
              return productChange.document.documentID == product.documentID;
            });
            isChange = true;
          } 
       else if (productChange.type == DocumentChangeType.added && !_isFirst) {
          _products.insert(0, productChange.doc);
          isChange = true;
   
      }
       else {

        if (productChange.type == DocumentChangeType.modified) {
          int indexWhere = _products.indexWhere((product) {
            return productChange.document.documentID == product.documentID;
          });

          if (indexWhere >= 0) {
            _products[indexWhere] = productChange.document;
          }
          isChange = true;
        }
      }
    });

    if(isChange) {
      _streamController.add(_products);
    }
  }

  @override
  void initState() {
    Firestore.instance
        .collection('products')
        .snapshots()
        .listen((data) => onChangeData(data.documentChanges));

    requestNextPage();
    super.initState();
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (scrollInfo.metrics.maxScrollExtent == scrollInfo.metrics.pixels) {
            requestNextPage();
          }
          return true;
        },
        child: StreamBuilder<List<DocumentSnapshot>>(
          stream: _streamController.stream,
          builder: (BuildContext context,
              AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
            if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.waiting:
                return new Text('Loading...');
              default:
                log("Items: " + snapshot.data.length.toString());
                return ListView.separated(
                  separatorBuilder: (context, index) => Divider(
                    color: Colors.black,
                  ),
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 32),
                    child: new ListTile(
                      title: new Text(snapshot.data[index]['name']),
                      subtitle: new Text(snapshot.data[index]['description']),
                    ),
                  ),
                );
            }
          },
        ));
  }

  void requestNextPage() async {
    if (!_isRequesting && !_isFinish) {
      QuerySnapshot querySnapshot;
      _isRequesting = true;
      if (_products.isEmpty) {
        _isFirst = true;
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .limit(5)
            .getDocuments();
      } else {
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .startAfterDocument(_products[_products.length - 1])
            .limit(5)
            .getDocuments();
      }

      if (querySnapshot != null) {
         _isFirst = false;
        int oldSize = _products.length;
        _products.addAll(querySnapshot.documents);
        int newSize = _products.length;
        if (oldSize != newSize) {
          _streamController.add(_products);
        } else {
          _isFinish = true;
        }
      }
      _isRequesting = false;
    }
  }
}
Ilyas Arafath
  • 511
  • 7
  • 13