Code Explanation
The provided code implements an infinite scrolling feature in a Flutter app that retrieves leaderboard data from Firebase Realtime Database. Here’s a breakdown of how the code works:
Initialization and Scroll Controller Setup:
The initState method is overridden to initialize the leaderboard data retrieval and set up a scroll listener.
_scrollController is created to monitor the scroll position.
Variable Declarations:
lastKey: Tracks the last retrieved key for pagination.
loadingMore: Flags whether more items are currently being loaded.
_scrollController: Controls the scrolling behavior.
Disposing the Scroll Controller:
The dispose method is overridden to properly dispose of the scroll controller.
Scroll Listener Method:
_scrollListener detects when the user reaches the end of the list and triggers _getLeaderboards to load more items if loadingMore is false.
Leaderboard Data Retrieval:
_getLeaderboards is an asynchronous method that fetches leaderboard data from Firebase.
It first checks for network connectivity using NetworkHelper.checkNetwork.
It then queries the Firebase database for the leaderboard items, using pagination to fetch items in batches of 10.
If lastKey is not null, it appends the new items to the existing list, updates the lastKey, and sorts the leaderboard.
Error Handling:
If an error occurs during data retrieval, it prints the error and resets loadingMore.
If there is no network, it shows a dialog prompting the user to retry.
Issue of Getting Repeated List
The issue of getting a repeated list likely arises due to the way pagination is handled. Specifically, the following points may contribute to this issue:
Incorrect Use of endAt for Pagination:
The query uses .endAt(lastKey), which includes the item with lastKey in the results. This can cause the last item of the previous batch to be included again in the new batch, leading to duplicates.
Appending Items Without Proper Filtering:
The retrieved items are appended directly to the leaderList without checking if they are already present. This can result in duplicate entries if the pagination query overlaps.
Resetting loadingMore Flag:
The loadingMore flag is reset after sorting and processing the leaderboard, but duplicates might already be added by that time.
`
@override
void initState() {
_getLeaderboards();
super.initState();
_scrollController.addListener(_scrollListener);
}
// Define a variable to track the last retrieved key for pagination
String? lastKey;
// Define a variable to track whether more items are being loaded
bool loadingMore = false;
// Set up a ScrollController
final ScrollController _scrollController = ScrollController();
// Dispose the ScrollController in dispose
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// Scroll listener method to detect when the user reaches the end of the list
void _scrollListener() {
if (!_scrollController.position.outOfRange &&
_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// User has scrolled to the end of the list, load more items
if (!loadingMore) {
_getLeaderboards(); // Load more items
}
}
}
// Modify _getLeaderboards to load more items
_getLeaderboards() async {
setState(() {
loadingMore = true; // Set loading flag
});
bool network = await NetworkHelper.checkNetwork();
if (network) {
final dbRef = FirebaseDatabase.instance.ref().child('LeaderBoard');
Query query = dbRef
.orderByChild('stars')
.limitToLast(10); // Fetching 10 items initially
if (lastKey != null) {
query = query.endAt(lastKey); // Exclude the last retrieved item
}
query.once().then((DatabaseEvent event) {
Map<dynamic, dynamic>? map = event.snapshot.value as Map?;
if (map != null) {
map.forEach((key, value) {
Leaderboard leaderboard = Leaderboard.fromSnapshot(value);
setState(() {
leaderList.add(leaderboard);
lastKey = key;
});
// Update lastKey for next pagination
});
setState(() {
// Sort the list and handle pagination
leaderList.sort((a, b) => a.stars!.compareTo(b.stars!));
leaderList = leaderList.reversed.toList();
if (leaderList.isNotEmpty) {
gold = leaderList.elementAt(0);
leaderList.removeAt(0);
}
if (leaderList.isNotEmpty) {
silver = leaderList.elementAt(0);
leaderList.removeAt(0);
}
if (leaderList.isNotEmpty) {
bronze = leaderList.elementAt(0);
leaderList.removeAt(0);
}
loadingMore = false; // Reset loading flag
});
}
}).catchError((error) {
print("Error fetching data: $error");
// Handle error here
loadingMore = false; // Reset loading flag
});
} else {
// Show dialog if there's no network
if (context.mounted) {
showDialog(
context: context,
builder: (_) => CustomAlert(
onCancel: () {},
onConfirm: () {
Navigator.pop(context);
_getLeaderboards();
},
desc: 'No NetworknPlease connect and retry',
image: 'images/icons/no_wifi.png',
confirmText: "Retry",
isSingleButton: true,
),
);
}
loadingMore = false; // Reset loading flag
}
}
Tried Solution
To resolve the issue of repeated items, you should modify the pagination logic to exclude the last retrieved item and ensure no duplicates are appended. Here’s a suggested fix:
Modify Pagination Query:
Use .startAfter(lastKey) instead of .endAt(lastKey) to exclude the last retrieved item from the new batch.
Check for Duplicates Before Appending:
Ensure that the new items are not already present in the leaderList before appending them.
Here’s the updated part of the code:
// Modify _getLeaderboards to load more items
_getLeaderboards() async {
setState(() {
loadingMore = true; // Set loading flag
});
bool network = await NetworkHelper.checkNetwork();
if (network) {
final dbRef = FirebaseDatabase.instance.ref().child('LeaderBoard');
Query query = dbRef
.orderByChild('stars')
.limitToLast(10); // Fetching 10 items initially
if (lastKey != null) {
query = query.startAfter(lastKey); // Exclude the last retrieved item
}
query.once().then((DatabaseEvent event) {
Map<dynamic, dynamic>? map = event.snapshot.value as Map?;
if (map != null) {
map.forEach((key, value) {
Leaderboard leaderboard = Leaderboard.fromSnapshot(value);
if (!leaderList.any((item) => item.key == leaderboard.key)) {
setState(() {
leaderList.add(leaderboard);
lastKey = key;
});
}
});
setState(() {
// Sort the list and handle pagination
leaderList.sort((a, b) => a.stars!.compareTo(b.stars!));
leaderList = leaderList.reversed.toList();
if (leaderList.isNotEmpty) {
gold = leaderList.elementAt(0);
leaderList.removeAt(0);
}
if (leaderList.isNotEmpty) {
silver = leaderList.elementAt(0);
leaderList.removeAt(0);
}
if (leaderList.isNotEmpty) {
bronze = leaderList.elementAt(0);
leaderList.removeAt(0);
}
loadingMore = false; // Reset loading flag
});
}
}).catchError((error) {
print("Error fetching data: $error");
// Handle error here
setState(() {
loadingMore = false; // Reset loading flag
});
});
} else {
// Show dialog if there's no network
if (context.mounted) {
showDialog(
context: context,
builder: (_) => CustomAlert(
onCancel: () {},
onConfirm: () {
Navigator.pop(context);
_getLeaderboards();
},
desc: 'No NetworknPlease connect and retry',
image: 'images/icons/no_wifi.png',
confirmText: "Retry",
isSingleButton: true,
),
);
}
setState(() {
loadingMore = false; // Reset loading flag
});
}
}
Sudhanshu Saurabh Gupta is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.