8

Note: I have already seen answers on Lazy Load of ListView but those are for custom API not firestore database!

I have a books summary app, App fetches data from my Firebase/Firestore database and then display it using a ListView.builder which is wrapped inside a StreamBuilder.

Now, I want to fetch data lazily, I mean as the user scrolls through the List the required data gets loaded rather than loading data at once and then displaying it lazily.

//The Widget used to display data:

Widget feed() {
  return Container(
    width: deviceWidth,
    height: deviceHeight / 3,
    child: StreamBuilder(
        stream: Firestore.instance
            .collection('feedItem')
            .orderBy('feedId', descending: true)
            .snapshots(),

        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.hasData) {
            int totalLength = snapshot.data.documents.length;
            return ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: totalLength > 10 ? 10 : totalLength,
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  width: deviceWidth / 2.5,
                  child: GestureDetector(
                    onTap: () {
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (BuildContext context) => FeedIntro(
                                  snapshot.data.documents[
                                      ((totalLength - 1) - index)]['feedId'])));
                    },
                    child: Card(
                        child: Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: <Widget>[
                        Container(
                          // width: 150,
                          height: 150,
                          foregroundDecoration: BoxDecoration(
                              image: DecorationImage(
                                  image: NetworkImage(
                                    snapshot.data.documents[index]['feedImage'],
                                  ),
                                  fit: BoxFit.fill)),
                        ),
                        Center(
                            child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text(snapshot.data.documents[index]['title']),
                        )),
                      ],
                    )),
                  ),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Center(child: Text('Sorry Something went wrong!'));
          } else {
            return Center(
              child: SizedBox(
                child: CircularProgressIndicator(),
                width: 50,
                height: 50,
              ),
            );
          }
        }),
  );
}
8
  • 2
    What you call "lazy loading", many others call "pagination". Firestore has that capability. firebase.google.com/docs/firestore/query-data/query-cursors Commented Jun 2, 2019 at 3:03
  • 1
    To add to @DougStevenson comment you could pair that with the flutter firestore API (as I think the firebase team has yet to add Dart examples) from here: pub.dev/documentation/cloud_firestore/latest/cloud_firestore/… Also, these methods were recently added to the Dart firestore plugin (the incomplete API of the dart implementation is likely why the team has yet to add dart examples). :) Commented Jun 2, 2019 at 4:03
  • Thanks for giving your time! I got it I can use either limit or startAfter to fetch data in pieces but any suggestion on what approach I should use to integrate, this with my current code (StreamBuilder and ListView.Builder)? Commented Jun 2, 2019 at 10:18
  • @RajDhakad got any solution yet? Commented Nov 27, 2019 at 13:02
  • @ShrutiRamnandanSharma as far as I remember, all the data from firestore is cached, so after the first time(when it's not cached), it will automatically be fetched lazily. Commented Nov 27, 2019 at 13:17

2 Answers 2

7

The description of your lazy loading seems to match what pagination is. Here's a simple demo using Firestore with pagination in a ListView.builder

This sample implements snippets from Firebase official doc for Firestore pagination.

There are two ways to load the data on the view in this demo.

  • Refresh the entire ListView using RefreshIndicator
  • Scroll down to hit the bottom of the list to load up the next documents in the ListView. ScrollController is used to determine if the user has hit the bottom part of the list.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'DocObj.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Initialize Firebase
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  var scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    getDocuments();
    scrollController.addListener(() {
      if (scrollController.position.atEdge) {
        if (scrollController.position.pixels == 0)
          print('ListView scroll at top');
        else {
          print('ListView scroll at bottom');
          getDocumentsNext(); // Load next documents
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: listDocument.length != 0
            ? RefreshIndicator(
                child: ListView.builder(
                  physics: AlwaysScrollableScrollPhysics(),
                  controller: scrollController,
                  itemCount: listDocument.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text('${listDocument[index].documentName}'),
                    );
                  },
                ),
                onRefresh: getDocuments, // Refresh entire list
              )
            : CircularProgressIndicator(),
      ),
    );
  }

  List<DocObj> listDocument;
  QuerySnapshot collectionState;
  // Fetch first 15 documents
  Future<void> getDocuments() async {
    listDocument = List();
    var collection = FirebaseFirestore.instance
        .collection('sample_pagination')
        .orderBy("name")
        .limit(15);
    print('getDocuments');
    fetchDocuments(collection);
  }

  // Fetch next 5 documents starting from the last document fetched earlier
  Future<void> getDocumentsNext() async {
    // Get the last visible document
    var lastVisible = collectionState.docs[collectionState.docs.length-1];
    print('listDocument legnth: ${collectionState.size} last: $lastVisible');

    var collection = FirebaseFirestore.instance
        .collection('sample_pagination')
        .orderBy("name").startAfterDocument(lastVisible).limit(5);

    fetchDocuments(collection);
  }

  fetchDocuments(Query collection){
    collection.get().then((value) {
      collectionState = value; // store collection state to set where to start next
      value.docs.forEach((element) {
        print('getDocuments ${element.data()}');
        setState(() {
          listDocument.add(DocObj(DocObj.setDocDetails(element.data())));
        });
      });
    });
  }
}

To parse the data inside the document, you can create a model for your object.

class DocObj {
  var documentName;

  DocObj(DocObj doc) {
    this.documentName = doc.getDocName();
  }

  dynamic getDocName() => documentName;

  DocObj.setDocDetails(Map<dynamic, dynamic> doc)
      : documentName = doc['name'];
}

The sample handles this data from Firestore.

firestore dashboard

Here's how the app looks when running.

demo

Sign up to request clarification or add additional context in comments.

3 Comments

ListView.builder does the work. A beginner mistake is adding ListView.builder in a SingleChildScrollView which tries to load everything at once making the ListView.builder useless or sometimes loading the data directly into a SingleChildScrollView. I hope this comment helps some beginners.
Is there a way to show loading at the end ?
The author is asking for loading data with pagination with realtime data from a snapshot stream. You are just fetching chunks of data here with loosing the capability to get realtime updates.
2

I have suffered this,and what i did is i have added scrollcontroller in listview builder and i traced if it reaches bottom of the list and if it reaches at the bottom,then update the limit of the snapshot.

I have experimented this and works well.

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

class Sample extends StatefulWidget {
  @override
  _SampleState createState() => _SampleState();
}

class _SampleState extends State<Sample> {
  ScrollController _chatScrollController;
  int loadMoreMsgs = 25; // at first it will load only 25
  int a = 50; // 'loadMoreMsgs' will added by 'a' if we load more msgs in listview.
  
  @override
  void initState() {
    _chatScrollController = ScrollController()
      ..addListener(() {
        if (_chatScrollController.position.atEdge) {
          if (_chatScrollController.position.pixels == 0)
            print('ListView scrolled to top');
          else {
            setState(() {
              loadMoreMsgs =  loadMoreMsgs + a;
            });
            print('ListView scrolled to bottom');
          }
        }
      });
    super.initState();
  }
  
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: FirebaseFirestore.instance.collection('CollectionName').limit(loadMoreMsgs).snapshots(),
      builder: (context, snapshot) {
        return ListView.builder(
          controller: _chatScrollController,
          itemBuilder: (context, index) {
            return Text('This is a sample');
          },
        );
      },
    );
  }
}

I have only written required codes for understanding.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.