I am integrating a chat feature in my mobile application, and decided to use Firebase Realtime Database for the backend instad of Firestore as a cost reduction mechanism. I am running into a problem, however. There seems to be very sparse documentation on how to create infinite scrolling using data from Realtime Database
instead of Firestore
.
Below is the organization of my chat messages. This is the query I want to use:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
And this is the widget I want to return for each result:
MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture: message.profilePicture,
);
Here is the database structure
The query works, and I have already programmed the MessageWidget from the JSON response of the query. All I need is for the query to be called whenever it reaches the top of its scroll, and load more MessageWdiget
s. Also note, this is a chat app where users are scrolling up, to load older messages, to be added above the previous.
Thank you!
EDIT: here is the code I currently have:
Flexible(
child: StreamBuilder(
stream: FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.limitToLast(20)
.onValue,
builder:
(context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
} else {
Map<dynamic, dynamic> map =
snapshot.data!.snapshot.value as dynamic;
List<dynamic> list = [];
list.clear();
list = map.values.toList();
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ListView.builder(
controller: _scrollController,
// shrinkWrap: true,
itemCount: list.length,
itemBuilder: (context, index) {
final json = list[index]
as Map<dynamic, dynamic>;
final message = Message.fromJson(json);
return MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture:
message.profilePicture,
);
}),
),
);
}
},
),
),
My initState
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
//add more messages
} else {
print('At the bottom');
}
}
});
}
After several days of testing code, I came up with the following solution
The first step is to declare a ScrollController
in your state class.
final ScrollController _scrollController = ScrollController();
You will also need to declare a List
to store query results
List list = [];
Next, use the following function to get initial data
getStartData() async {
//replace this with your path
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('messages/${widget.placeID}');
starCountRef
.orderByChild("timeStamp")
//here, I limit my initial query to 6 results, change this to how many
//you want to load initially
.limitToLast(6)
.onChildAdded
.forEach((element) {
setState(() {
list.add(element.snapshot.value);
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
});
}
Run this in initState
void initState() {
super.initState();
FirebaseDatabase.instance.setPersistenceEnabled(true);
getStartData();
}
Now to display the initial data that was generated when the page was loaded, build the results into a ListView
ListView.builder(
itemCount: list.length,
controller: _scrollController,
//here I use a premade widget, replace MessageWidget with
//what you want to load for each result
itemBuilder: (_, index) => MessageWidget(
message: list[index]["text"],
date: list[index]["timeStamp"],
id: list[index]["uid"],
profilePicture: list[index]["profilePicture"],
name: list[index]["name"],
lastSender: list[index]["lastSender"],
),
),
Note that your ListView
must be constrained, meaning that you can scroll to the beginning or end of your ListView
. Sometimes, the ListView
won't have enough data to fill and be scrollable, so you must declare a height with a Container
or bound it to its contents.
Now you have the code that fetches data when the page is loaded using getStartData()
and initState
. The data is stored in list
, and a ListView.builder
builds a MessageWidget
for each item returned by getStartData
. Now, you want to load more information when the user scrolls to the top.
getMoreData() async {
var moreSnapshots = await FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.endBefore(list[0]["timeStamp"])
.limitToLast(20)
.once();
var moreMap = moreSnapshots.snapshot.value as dynamic;
setState(() {
list.addAll(moreMap.values.toList());
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
}
Then, make the function run when the ListView.builder
is scrolled all the way to the top by adding this to the already existing initState
.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
getMoreData();
}
}
});
Hopefully this helps or gives you a pointer on where to go from here. Thanks to Frank van Puffelen for his help on which query to use based on the previous answer.