Sort messages in Realtime DB by timestamp - flutter

I am having trouble by getting my messages stored in realtime db in order.
Here's my db structure
messages: {
$chatId: {
$messageId: {
timestamp: 1664884736728,
sender:"36a72WVw4weQEoXfk3T9gCtOL9n2",
message: "Hello world"
}
}
}
This is my chat repository
//get all messages of a chat
Query getMessages(String chatId) {
//get all messages of a chat
final messages = _database.ref().child("messages/$chatId");
//return all messages of a chat
return messages;
}
}
and this is how I am displaying it
StreamBuilder(
stream: _chatRepository.getMessages(chatId).onValue,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.data != null &&
snapshot.data?.snapshot?.value != null &&
snapshot.hasData) {
final messages = Map.from(snapshot.data?.snapshot.value as Map);
return ListView.builder(
itemCount: messages.length,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
final currChat = messages.values.toList()[index];
return BubbleCustom(...),
);
},
);
}
return Container();
},
),
This is my index in realTimeDB
{
"rules": {
"messages": {
"$chatId": {
".indexOn": ["timestamp"]
}
}
}
}
I need to get them in timestamp order. Is there any way I can do this? I hope you can help me. Thanks in advance!

You can use orderByChild to get the messages in the order of a specific child. So there that'd be:
stream: _chatRepository.getMessages(chatId).orderByChild('timestamp').onValue
Don't forget to define an index for timestamp, so that the sorting can be done on the database server.
To learn more on this, see the Firebase documentation on sorting and filtering data.

I solved it by doing it in the frontend like this:
final messages = Map.from(snapshot.data?.snapshot.value as Map);
//sort messages by timestamp ascending order
final messagesList = messages.values.toList();
messagesList.sort((a, b) => a["timestamp"].compareTo(b["timestamp"]));
final newMessagelist = List.from(messagesList.reversed);
return ListView.builder(
physics: const BouncingScrollPhysics(),
reverse: true,
itemCount: newMessagelist.length,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
final currChat = newMessagelist[index];
return BubbleCustom(
text: currChat["message"],
isSender: currChat["sender"] == senderUser.id,
tail: true,
textStyle: TextStyle(
fontSize: 16,
color: currChat["sender"] == senderUser.id ? Colors.white : Colors.black,
),
);
},
);
with reverse: true and inverting the list before I render it.

Related

How to build seperatorBuilder before itemBuilder in listview.seperated

I'm trying to implement a chat screen in my app and I want to display the dates before the conversation ( today, yesterday in most chatting apps ).
To display messages I use the itemBuilder and to show these dates I used seperatorBuilder of Listview.seperated.
The problem is that the date widget appears after the message, which was supposed to indicate the date.
(here, the 'yesterday' widget was supposed to come above the 'hey' message). (the seperatorBuilder builds after itemBuilder)
I thought to switch the codes of seperatorBuilder and itemBuilder (build the chat bubble in seperatorbuilder and dater widget in itemBuilder) to get the build order I want but I doubt its performance.
CODE:
ListView.separated(
separatorBuilder: (context, index) {
if (index == 0 ||
compareDate(
currentSnap: snapshot.data!.docs[index] //snapshot from StreamBuilder
.data()['serverTimeStamp'],
previousSnap: snapshot.data!.docs[index - 1]
.data()['serverTimeStamp'])) // some checks before displaying
{return chatDater(); // the Widget that displays date container
} else {
return SizedBox(
width: 0,
height: 0,
);
}
},
controller: _controller1,
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index)
return MessageBubble(
timestamp:
snapshot.data!.docs[index].data()['serverTimeStamp'],
message: snapshot.data!.docs[index].data()['message'],
IsYou: snapshot.data!.docs[index].data()['sender'] ==
widget.usernameOfFriend
? false
: true,
);
},
)
the compareDate method:
checks if two consecutive messages have different dates, if yes then returns true (so that we can display the dater widget)
bool compareDate(
{required Timestamp currentSnap, required Timestamp previousSnap}) {
var currentDate = currentSnap.toDate();
var previousDate = previousSnap.toDate();
if (currentDate.year != previousDate.year) return true;
if (currentDate.month != previousDate.month) return true;
if (currentDate.day != previousDate.day) return true;
return false;
}
Add timestamp also in itemBuilder when needed. Here is the simplified version:
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
final previous = index > 0 ? messages[index - 1] : null;
final current = messages[index];
final date = showDate(current, previous);
return Column(
children: [
if (date != null)
Text(date.toString()),
Text(current.text),
]
);
},
);
}
String? showDate(Message current, Message? previous) {
// TODO control when date is shown
if (previous == null) {
return current.date.toString();
} else {
return null;
}
}

Adding New Data in Flutter using Firestore, Result in Redundant Data?

I was trying to get data from firestore using class defined like this one:
Future<QuerySnapshot<Map<String, dynamic>>> getPubs() async {
queryPubs = await pubsDb.get();
if (queryPubs!.docs.isEmpty) {
log("[getPubs] Variable queryPubs is empty");
} else {
await pubData(queryPubs!.docs).then((value) {
if (value.isNotEmpty) {
isPub.value = true;
log("[getPubs] Variable pubAll is not empty");
} else {
log("[getPubs] Variable pubAll is empty");
}
});
}
return queryPubs!;
}
Future<List<Map<String, dynamic>>> pubData(List<DocumentSnapshot>? docs) async {
for (int i = 0; i < docs!.length; i++) {
DbPublisher dbPublisher = DbPublisher.fstoreInfo(docs[i]);
final imgUrl = await getImgUrl(dbPublisher.imgName, DataType.PUBLISHER);
pubInfo = {
"name": dbPublisher.name,
"imgURL": imgUrl,
"imgName": dbPublisher.imgName,
"desc": dbPublisher.desc,
"email": dbPublisher.email,
"active": dbPublisher.active,
"status": dbPublisher.status,
"albums": dbPublisher.albums,
"songs": dbPublisher.songs,
"state": dbPublisher.state,
"privilege": dbPublisher.privilege,
"payout": dbPublisher.payout,
"compensation": dbPublisher.compensation,
"started": dbPublisher.started,
"due": dbPublisher.due,
"license": dbPublisher.license,
"duration": dbPublisher.duration,
"valid": dbPublisher.valid,
};
pubAll!.add(pubInfo);
}
return pubAll!;
}
I get the data, but when I was about to add one more data; the new data doesn't get displayed. Instead, it was previous data and there were redundant of that. I used the latest package of firestore. Did someone ever experience this one? I used GetX dependency injection for all of this. I used GetxService as it made it as permanent.
Update: redundant here I meant there is the same data over and over. But when I restarted the app, all the data return normal as it used to be. I wonder what is going on?
Update:
The flutter code
Expanded(
child: FutureBuilder<QuerySnapshot<Map<String, dynamic>>>(
future: dNd.getPubs(),
builder: (context, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
listAlbs = snapshot.data!.docs;
if (listAlbs.isNotEmpty) {
totalAlbs = listAlbs.length;
log("Load $totalAlbs albums from Firestore");
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const ScrollPhysics(),
controller: scrollCtrl,
itemCount: listAlbs.length,
itemBuilder: (context, index) {
return albsFstore(context, dNd.pubAll![index], index);
},
);
} else {
return const Center(child: FavText("The album data is empty", 15.0));
}
} else if (snapshot.hasError) {
return const Center(child: FavText("The server is error", 15.0));
} else {
return const Center(child: FavText("The data is empty", 15.0));
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return AlignPositioned(
dx: 0,
dy: -(MediaQuery.of(context).size.height / 4),
child: Container(
width: 128,
height: 128,
padding: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(borderRadius:
BorderRadius.all(Radius.circular(16.0)), color: Color.fromARGB(88, 44, 44, 44)),
child: const LoadingIndicator(indicatorType: Indicator.ballRotateChase, colors: rainbowColors),),);
}
return const SizedBox();
},
),
),
Cloud Firestore gives you the ability to read the value of a collection or a document. This can be a one-time read, or provided by real time updates when the data within a query changes.
For basic write operations, you can use set() to save data to a specified reference, replacing any existing data at that path and to read a collection or document once, call the Query.get or DocumentReference.get methods.
As recommended in comments, you should check these examples once Duplicate Data from Firestoreand How to get data from Firestore.
Also go through this useful document for one time read implementation.

Display ads in every nth index using Listview.builder

I am trying to place an advertisement after every 3rd index. I got the required output but my problem in my case is i have a two type of List,
List One has 50 datas in it.
List two has only 5 datas.
When i tried with the below code.
ListView.separated(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(15),
shrinkWrap: true,
itemCount: articles.length,
separatorBuilder: (context, index) => SizedBox(
height: 15,
),
itemBuilder: (BuildContext context, int index) {
final Article article = articles[index];
final Sponsor sponsor = sponsors[index];
if(index %3 == 0 && index != 0) return SponsorCard(sponsor: sponsor, scaffoldKey: widget.scaffoldKey);
return Card4(article: article, heroTag: 'recent${article.id}', scaffoldKey: widget.scaffoldKey);
widget.scaffoldKey);
},
),
i am getting range error since sponsor list reached its last position.
error
The following RangeError was thrown building:
RangeError (index): Invalid value: Not in inclusive range 0..4: 49
What i need is that i need to make the sponsor list as loop so that if the List reached its last position it has to move again to the first position.
Can someone please help me on this.
You can create a new combined list as,
and make sure both Article & Sponsor class should be child class of ListItem.
OR
you need to wrap Article & Sponsor classes under ListItem Wrapper class
List items = []
Now use the items with ListView :
ListView.separated(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(15),
shrinkWrap: true,
itemCount: items.length,
separatorBuilder: (context, index) => SizedBox(
height: 15,
),
itemBuilder: (BuildContext context, int index) {
if (item[index] is Articles){
return SponsorCard(..);
}else{
return Card4(...);
}
},
);
Added Example on Request comment:
class _ListItem {
final bool isArticleType;
final dynamic value;
_ListItem(this.isArticleType, this.value);
}
class Article {}
class Sponsor {}
List<_ListItem> createList(List<Article> articles, List<Sponsor> sponsors) {
List<_ListItem> items = [];
items.addAll(articles.map((e) => _ListItem(true, e)).toList());
var index = 0;
var sIndex = 0;
for (var i = 0; i < articles.length && sIndex < sponsors.length; i++) {
if (i != 0 && i % 3 == 0) {
items.insert(index, _ListItem(false, sponsors[sIndex]));
sIndex++;
index++;
}
index++;
}
return items;
}
Thanks for your response guys.
I could achieve by the below code.
itemBuilder: (BuildContext context, int index) {
actualIndex = actualIndex +1;
if(sponsorIndex+1 >= sponsors.length){
sponsorIndex = -1;
}
if(index == 0 ){
articleIndex = -1;
}
if ((actualIndex-tempIndex) % 3 == 0 && actualIndex != 0 && (actualIndex-prevIndex) >=3 ) {
tempIndex = tempIndex +1;
prevIndex = actualIndex;
sponsorIndex = sponsorIndex + 1;
final Sponsor sponsor = sponsors[sponsorIndex];
return SponsorCard(sponsor: sponsor, scaffoldKey: widget.scaffoldKey);
}
articleIndex = articleIndex + 1;
final Article article = articles[articleIndex];
return Card4(article: article, heroTag: 'recent${article.id}', scaffoldKey: widget.scaffoldKey);
},

How to work with listviews correctly for displaying text

I'm trying to display an message, but unfortunately I think I'll need to change my method.
So I've resumed some code
child: FutureBuilder<List<Product>>(
future: products,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final products = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(10.0),
child: ListView.builder(
itemCount: products.length, // currently has 400 items
itemBuilder: (context, index) {
if (difference == 0) {
return cardUI(
...
);
} else {
return Text('Nothing to display!');
}
}));
})),
How can I manage to return the message only one time? Do I need to change all the code? Since it's displaying almost 250 times 'Nothing to display'
Edit:
This is what I'm using to calculate the difference!
DateTime productExpire = DateTime.parse(products[index].date);
final productDate = productExpire;
final today = DateTime.now();
final difference = calcDays(
today, productDate);
The solution that comes to mind is to make products only equal to the products that have a difference of days from today of zero, so then based on products.length you can either return a Text() (if products.length == 0) or call ListView.builder (if products.length > 0).
Basically:
Instead of this:
All products
products = [thisProdHasDifferenceOfZero, thisOneDoesnt, thisOneDoes, thisOneDoesnt, ...]
(products.length == 400 every time)
You can just have:
Only products that you want to work with
products = [thisProdHasDiffOfZero, thisOneToo, thisOneToo, ...]
(products.length <= 400)
In your code:
Instead of calculating the difference your way, use this:
This method calculates the difference between two dates, the one you're using may run into some bugs... check this answer for more information
int daysBetween(DateTime from, DateTime to) {
from = DateTime(from.year, from.month, from.day);
to = DateTime(to.year, to.month, to.day);
return (to.difference(from).inHours / 24).round();
}
Then:
child: FutureBuilder<List<Product>>(
future: products,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final today = DateTime.now();
final products = (snapshot.data! as List<Product>).where((product) => daysBetween(product.date, today) == 0).toList;
// Now you know that every product inside 'products', if there's any, has a day difference of 0.
return Padding(
padding: const EdgeInsets.all(10.0),
// 'products' can be empty since you may have 0 products that have the difference you are looking for.
// In that case you return the text.
child: (products.length == 0) ?
return Text('Nothing to display!');
: return ListView.builder( // If products has one or more items...
itemCount: products.length,
itemBuilder: (context, index) {
return cardUI(
...
);
}
)
...
You want to show a single error if the listview.builder does not have any data right? Hope I am not getting you wrong. If so you can use the ternary operator where you have mentioned the listview.builder to make it conditional.
or you can try to update the item count with condition.
itemcount: difference ==0? products.length : 1,
After getting all products filter them and create another list of product that will satisfy difference==0.
final products = snapshot.data!;
List<Product> temp = [];
// temp = products.where((p) => validation(p) == 0).toList();
for(final p in products)
{
int difference = yourValidation(p);
if(difference == 0) temp.add(p);
}
//...
child: ListView.builder(
itemCount: temp.length,
More about List

Get Firebase Document within ListView Builder

I got a list of questions. When the user doesn't like the question, it can be added to a hidden list. Now I would like list all the questions which have been added to the hidden list.
The Firestore IDs are added to an array within a provider (setting).
When I build the ListView I want to fetch the question documents by document id and pass those document fields to the HiddenList widget.
I've tryied using StreamBuilder, Future,.. unfortunately nothing worked so far..
Any pointers?
Code:
var questions = FirebaseFirestore.instance.collection('questions');
if (setting.hidden.length == 0) {
return Text('Empty');
} else {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: setting.hidden.length,
itemBuilder: (context, index) {
return new StreamBuilder(
stream: questions.doc('${setting.hidden[index]}').snapshots(),
builder: (context, docSnapshot) {
if (!docSnapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
var data = docSnapshot.data!;
return HiddenList(
de_du: data['de_du'],
de_sie: data['de_sie'],
de_ich: data['de_ich'],
en: data['en'],
id: setting.hidden[index],
);
}
});
},
);
}