Reading data from firebase is expensive and if there is lot of data, only small portion of it would be visible to user. So why download all of it if we can use pagination method to achieve infinite scroll list.
This is our data in Firebase Firestore.
We will start by creating a new flutter project. I will try to explain the steps required to achieve infinite scroll in flutter using firebase firestore.
Create a data model class
class ColorDetails { final String label; final int code; ColorDetails(this.code, this.label); factory ColorDetails.fromMap(Map<String, dynamic> json) { return ColorDetails(json['color_code'], json['color_label']); } Map toJson() { return { 'color_code': code, 'color_label': label, }; } }
Query to fetch data from Firestore
Query _query = FirebaseFirestore.instance .collection("sample_data") .orderBy('color_label') .limit(PAGE_SIZE);
Here
PAGE_SIZE
is the number of items we want in each page. For this tutorial, it's 30.static const PAGE_SIZE = 30;
We will get the paged data from firebase in following way
final List<ColorDetails> pagedData = await _query.get().then((value) { if (value.docs.isNotEmpty) { _lastDocument = value.docs.last; } else { _lastDocument = null; } return value.docs .map((e) => ColorDetails.fromMap(e.data() as Map<String, dynamic>)) .toList(); });
We are saving the last document send which we will use while making call for next page.
DocumentSnapshot? _lastDocument;
Once we get the data, we can call
setState
setState(() { _data.addAll(pagedData); if (pagedData.length < PAGE_SIZE) { _allFetched = true; } _isLoading = false; });
Following variables needs to be defined at top for this to work. Hence
bool _allFetched = false; bool _isLoading = false; List<ColorDetails> _data = [];
Now, we can show the data to the ListView
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: ListView.builder( itemBuilder: (context, index) { if (index == _data.length) { return Container( key: ValueKey('Loader'), width: double.infinity, height: 60, child: Center( child: CircularProgressIndicator(), ), ); } final item = _data[index]; return ListTile( key: ValueKey( item, ), tileColor: Color(item.code | 0xFF000000), title: Text( item.label, style: TextStyle(color: Colors.white), ), ); }, itemCount: _data.length + (_allFetched ? 0 : 1), ), ); }
We will ScrollEndNotification NotificationListener and it's
onNotification
argument will beonNotification: (scrollEnd) { if (scrollEnd.metrics.atEdge && scrollEnd.metrics.pixels > 0) { _fetchFirebaseData(); } return true; },
The
_fetchFirebaseData()
is the method where we define our query, getting the data and calling setState. But the logic to get the data is still missing. So let's modify the query a little bit,Query _query = FirebaseFirestore.instance .collection("sample_data") .orderBy('color_label'); if (_lastDocument != null) { _query = _query.startAfterDocument(_lastDocument!).limit(PAGE_SIZE); } else { _query = _query.limit(PAGE_SIZE); }
- Everything else remains same. You will see following output
- Here is the full code
#pubspec.yaml
name: flutter_firestore_infinite_scroll
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
firebase_core: "^1.4.0"
cloud_firestore: "^2.4.0"
flutter:
uses-material-design: true
//main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
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,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const PAGE_SIZE = 30;
bool _allFetched = false;
bool _isLoading = false;
List<ColorDetails> _data = [];
DocumentSnapshot? _lastDocument;
@override
void initState() {
super.initState();
_fetchFirebaseData();
}
Future<void> _fetchFirebaseData() async {
if (_isLoading) {
return;
}
setState(() {
_isLoading = true;
});
Query _query = FirebaseFirestore.instance
.collection("sample_data")
.orderBy('color_label');
if (_lastDocument != null) {
_query = _query.startAfterDocument(_lastDocument!).limit(PAGE_SIZE);
} else {
_query = _query.limit(PAGE_SIZE);
}
final List<ColorDetails> pagedData = await _query.get().then((value) {
if (value.docs.isNotEmpty) {
_lastDocument = value.docs.last;
} else {
_lastDocument = null;
}
return value.docs
.map((e) => ColorDetails.fromMap(e.data() as Map<String, dynamic>))
.toList();
});
setState(() {
_data.addAll(pagedData);
if (pagedData.length < PAGE_SIZE) {
_allFetched = true;
}
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: NotificationListener<ScrollEndNotification>(
child: ListView.builder(
itemBuilder: (context, index) {
if (index == _data.length) {
return Container(
key: ValueKey('Loader'),
width: double.infinity,
height: 60,
child: Center(
child: CircularProgressIndicator(),
),
);
}
final item = _data[index];
return ListTile(
key: ValueKey(
item,
),
tileColor: Color(item.code | 0xFF000000),
title: Text(
item.label,
style: TextStyle(color: Colors.white),
),
);
},
itemCount: _data.length + (_allFetched ? 0 : 1),
),
onNotification: (scrollEnd) {
if (scrollEnd.metrics.atEdge && scrollEnd.metrics.pixels > 0) {
_fetchFirebaseData();
}
return true;
},
),
);
}
}
class ColorDetails {
final String label;
final int code;
ColorDetails(this.code, this.label);
factory ColorDetails.fromMap(Map<String, dynamic> json) {
return ColorDetails(json['color_code'], json['color_label']);
}
Map toJson() {
return {
'color_code': code,
'color_label': label,
};
}
}
This is my first article hence there might be few mistakes or presentation issue. Please let me know about them in the comment. I will try to improve and write more articles.