stefanmuke
stefanmuke

Reputation: 399

Create infinite list with Cloud Firestore in flutter

I am currently using Cloud Firestore with the Streambuilder widget in order to populate a ListView widget with Firestore documents.

new StreamBuilder<QuerySnapshot>(
  stream: Firestore.instance.collection('videos').limit(10).snapshots(),
  builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
    if (!snapshot.hasData) return new Center(
      child: new CircularProgressIndicator(),
    );
    return new ListView(
      children: snapshot.data.documents.map((DocumentSnapshot document) {
        new Card(child: ...)
      }).toList(),
    );
  },
);

This setup however only allows for the querying of the first x results (in this case x=10), with x being a fixed number that will sooner or later be exceeded by the number of Card widgets the user wants to see as he or she scrolls down.

Would it now be possible to query the first x results, and after the user hits a scroll threshold to query the next x+10 results from Cloud Firestore and so on?
This would allow for a dynamic list length which would also benefit the Firestore data consumption.

Upvotes: 15

Views: 5016

Answers (2)

Shyju M
Shyju M

Reputation: 9933

I am not sure whether it is possible or not with Streambuilder. I have integrated the similar functionality in my App using the startAfter method as shown below

class Feed extends StatefulWidget {
  Feed({this.firestore});

  final Firestore firestore;

  @override
  _FeedState createState() => _FeedState();
}

class _FeedState extends State<Feed> {
  ScrollController controller;
  DocumentSnapshot _lastVisible;
  bool _isLoading;
  CollectionReference get homeFeeds => widget.firestore.collection('homefeed');
  List<DocumentSnapshot> _data = new List<DocumentSnapshot>();
  final scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    controller = new ScrollController()..addListener(_scrollListener);
    super.initState();
    _isLoading = true;
    _getData();
  }

  Future<Null> _getData() async {
//    await new Future.delayed(new Duration(seconds: 5));
    QuerySnapshot data;
    if (_lastVisible == null)
      data = await widget.firestore
          .collection('homefeed')
          .orderBy('created_at', descending: true)
          .limit(3)
          .getDocuments();
    else
      data = await widget.firestore
          .collection('homefeed')
          .orderBy('created_at', descending: true)
          .startAfter([_lastVisible['created_at']])
          .limit(3)
          .getDocuments();

    if (data != null && data.documents.length > 0) {
      _lastVisible = data.documents[data.documents.length - 1];
      if (mounted) {
        setState(() {
          _isLoading = false;
          _data.addAll(data.documents);
        });
      }
    } else {
      setState(() => _isLoading = false);
      scaffoldKey.currentState?.showSnackBar(
        SnackBar(
          content: Text('No more posts!'),
        ),
      );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      key: scaffoldKey,
      appBar: new AppBar(),
      body: RefreshIndicator(
          child: ListView.builder(
        controller: controller,
        itemCount: _data.length + 1,
        itemBuilder: (_, int index) {
          if (index < _data.length) {
            final DocumentSnapshot document = _data[index];
            return new Container(
              height: 200.0,
              child: new Text(document['question']),
            );
          }
          return Center(
            child: new Opacity(
              opacity: _isLoading ? 1.0 : 0.0,
              child: new SizedBox(
                  width: 32.0,
                  height: 32.0,
                  child: new CircularProgressIndicator()),
            ),
          );
        },
      ),
        onRefresh: ()async{
            _data.clear();
            _lastVisible=null;
            await _getData();
        },
      ),
    );
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  void _scrollListener() {
    if (!_isLoading) {
      if (controller.position.pixels == controller.position.maxScrollExtent) {
        setState(() => _isLoading = true);
        _getData();
      }
    }
  }
}

Hope it helps!

Upvotes: 13

Frank van Puffelen
Frank van Puffelen

Reputation: 598765

That's definitely possible, but there's nothing pre-built in the API.

You'll have to remember the last document on the first page, and then startAfter() with that document to get the second page of documents.

See the documentation on Paginating Data with Query Cursors.

Upvotes: 2

Related Questions