Author’s note: If you like this post, consider supporting at patreon.com/windlejacob12. I love writing,
and sharing what I’ve learned. I’d love to spend the majority of my time doing it!
I’ve been working on a contact lately with a local startup in Johnson City, to implement infinite scrolling in their application. The codebase is entirely generated via
FlutterFlow, and being generated by a low-code tool it’s a ball of spaghetti.
Their feed page as initially implemented would just pull in the latest 200 posts. This static query worked really well for them starting out. However, they went
viral on social media (as one does) and we are now dealing with issues of scale. People are actually scrolling through the feed now! Also, additional screens that
contain up to thousands of users are crashing as they all get pulled into memory from Firebase at once. Oodles of StreamBuilders, normally an excellent widget to
choose in Flutter, were causing crashes as they all competed for what appeared to be a finite pool of connections to Firebase at once. iOS and Android would then
just kill the app. So what were we to do?
The idea of Infinite Scroll was floated, mimicking how other social media apps work by providing content in a paginated manner unbeknownst to the user. This would solve the
problems that we were having with app crashes, while also making things more efficient. Most users aren’t going to sit and scroll on the app for a very long time. To support
infinite scroll, queries that previously pulled in the entire collection of data would need to me modified. Data needed to be paginated. This page here details how to do it
from the excellent Firebase documentation: https://firebase.google.com/docs/firestore/query-data/query-cursors
TL;DR – Query cursors allow you to “checkpoint” where you are in the stream of data from Firestore, and then return the next batch of data either starting at or ending
at your checkpoint. Here’s the example that they gave on their site:
db.collection("cities").doc("SF").get().then(
(documentSnapshot) {
final biggerThanSf = db
.collection("cities")
.orderBy("population")
.startAt([documentSnapshot]);
},
onError: (e) => print("Error: $e"),
);
This uses asynchronous programming in Dart, plus a DocumentSnapshot to serve as our query cursor. You retrieve SF, then you get all cities bigger than SF using .startAt
towards the bottom of the inner function call.
Here’s the same code but with async/await instead.
try {
var sfDoc = await db.collection("cities").doc("SF").get();
} catch(e) {
print(e);
}
final biggerThanSf = await db
.collection("cities")
.orderBy("population")
.startAt([sfDoc])
.get();
This page, and these example queries gave me a great place to get started. The previous iteration of this screen had been through StreamBuilder
widgets that were passed
the query directly. The new iteration would do something a little more clever.
A Reusable Widget
Combining principles of both functional and object-oriented programming, and
armed with the knowledge that everything is a widget in Flutter, it should
be possible to represent our feed (and any other infinite scroll) as a
widget. The Infinite Scroll would only need to know the following:
- How large should each page be?
- What is the “base query” that we are working with?
- How can we control the loading of a new page?
- How do we build the widgets that represent our list items?
With these questions, our class begins to take shape. Here’s an example skeleton
class that attempts to answer all 4:
class InfiniteScrollFeed extends StatefulWidget {
int pageSize; // 1.
Query<Map<String,dynamic>> Function() makeFirebaseQuery; // 2.
Paginator paginator; // 3.
Widget Function(dynamic) itemBuilder; // 4.
ScrollController controller; // 5.
InfiniteScrollFeed(
{this.pageSize,
this.makeFirebaseQuery,
this.controller,
this.itemBuilder}) {
this.paginator = Paginator(pageSize: pageSize, queryFactory: makeFirebaseQuery);
}
}
- A plain
int
can hold our page size, and is used later in constructing a
paginator object. makeFirebaseQuery
is a function that returns a firebase query, to ensure that we have fresh firebase query references with each invocation.Paginator
is a TBD class that will handle the pagination of the query (and yield values)itemBuilder
is a function describing how to build a widget from the return of thePaginator
paging calls.ScrollController
will control loading of our pages through scroll events later.
This borrows from functional programming via makeFirebaseQuery
and itemBuilder
. makeFirebaseQuery
follows the factory pattern, in that it
is a factory that will churn out Firebase Query objects. This function can
return any firebase query that you would like, provided that it complies
with the correct type of Query
.
itemBuilder
is a generic description of how to build a widget. This allows
the InfiniteScrollFeed
class to build a list of widgets from whatever
data it retrieves from Firebase without knowing anything about the widget
details. It just calls the function it was given to render widgets.
This higher level class of InfiniteScrollFeed
will handle instrumenting our
paginated query, and displaying our widgets, but now we must go deeper.
We must look at pagination.
The Paginator
widget shown above as a part of InfiniteScrollFeed
(composition ftw),
is where the magic happens. We can modify the example shown in the first section on query cursors
to implement our new Paginator widget. The interface though, is very straightforward:
abstract class Paginator<T> {
Future<List<T>> next();
}
The Paginator of type T
should be able to return a Future
containing whatever page of data>
w