this is code of paginating data from firestore , here i am using a library called firebase_ui_firestore ^1.5.15 from pub.dev,pagination works fine but here the issue is when i tap on the textfield it automatically scrolls up the list , here i always want the list to be bottom for chat app, but for the first time when we click to the chat page it displays the list and scroll at bottom part of list and works as expected ,but the issue only happens when we click on the textfield or send any message the list scrolls up.
is this any library issue? or logical error please help.
here my main aim the list is always at the bottom when we trying to message someone or tap on the keypad or textfield. here below i am pasting the code of chatpage.
class _ChatAppState extends State<ChatApp> {
final TextEditingController _messageController = TextEditingController();
final ChatService _chatService = ChatService();
bool isImageSelected = false;
bool isSendingImage = false;
final ScrollController _scrollController = ScrollController();
XFile? image;
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
return Scaffold(
backgroundColor: ThemeManager.scaffoldBackgroundColor,
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ChatScreenAppBar(
senderName: widget.name, avatarUrl: widget.profileUrl),
Expanded(child: _buildMessageList()),
TextField(
onMessageSent: (text) {
sendMessage();
_scrollToBottom();
},
onImageSelected: (selectedImage) async {}, messageController: _messageController,
),
],
),
);
}
/// send chat message.
void sendMessage()async{
try{
if(_messageController.text.isNotEmpty)
{
await _chatService.sendMessage(widget.userId,
widget.currentUserId,
_messageController.text,'recieverEmail','text','gp-01');
_messageController.clear();
}else{
log('its empty');
}
}catch(e)
{
log("send Error: ${e.toString()}");
}
}
_scrollToBottom() {
if(_scrollController.hasClients)
{
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
Widget _buildMessageList() {
List<String> ids = [widget.currentUserId, widget.userId];
ids.sort();
String chatRoomId = ids.join("_");
return FirestoreQueryBuilder(
pageSize: 5,
query: FirebaseFirestore.instance
.collection('chat_rooms')
.doc(chatRoomId)
.collection('messages')
.orderBy('TimeStamp',descending: true),
builder: (context, snapshot, index) {
print("currrent index $index");
if (snapshot.hasError) {
return Text('Error ${snapshot.error}');
}
if (snapshot.isFetching) {
return const Center(child: CircularProgressIndicator());
}
print("firebase docs ${snapshot.docs}");
List<Message> allMessages = snapshot.docs.map((doc) {
return Message.fromFireStore(doc);
}).toList();
// Group messages by date
return GroupedListView<Message, DateTime>(
controller: _scrollController,
reverse: true,
order: GroupedListOrder.ASC,
floatingHeader: true,
elements:allMessages.toList(),
groupBy: (message) =>DateTime(
DateTime.parse(message.timeStamp.toDate().toString()).year,
DateTime.parse(message.timeStamp.toDate().toString()).month,
DateTime.parse(message.timeStamp.toDate().toString()).day,
),
itemComparator: (item1, item2) => item1.compareTo(item2),
sort: false, //
groupHeaderBuilder: (Message message) {
final formattedDate =
formatMessageDate(DateTime.parse(message.timeStamp.toDate().toString()));
return SizedBox(
height: 40.h,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
formattedDate,
style: const TextStyle(color: ThemeManager.primaryBlack),
),
),
),
);
},
itemBuilder: (context, Message message) {
// WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
int messageIndex = allMessages.indexOf(message);
final hasEndReached = snapshot.hasMore &&
messageIndex + 1 == snapshot.docs.length &&
!snapshot.isFetchingMore;
print("has reached the end: $hasEndReached");
if(hasEndReached) {
print("fetch more");
snapshot.fetchMore();
}
String messageId = snapshot.docs[messageIndex].id;
return Align(
alignment: message.receiverId == widget.userId
? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h),
child: Column(
children: [
message.receiverId == widget.userId ? Dismissible(
key: UniqueKey(),
confirmDismiss: (direction) async {
bool shouldDelete = await Dialogs.showDeleteConfirmationDialog(context);
return shouldDelete;
},
onDismissed: (direction) async{
_chatService.deleteMessage(widget.currentUserId, widget.userId, messageId);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TextMessageContainer(message: message,
senderId: widget.currentUserId,
receiverId:widget.userId,),
SizedBox(height: 5.h),
Text(
DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack,
),
),
],
),
):Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TextMessageContainer(message: message,
senderId: widget.currentUserId,
receiverId:widget.userId,),
SizedBox(height: 5.h),
Text(
DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack,
),
),
],
),
],
),
),
);
}
);
},
);
}
String formatMessageDate(DateTime? dateTime) {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
if (dateTime?.year == now.year &&
dateTime?.month == now.month &&
dateTime?.day == now.day) {
return "Today";
} else if (dateTime?.year == yesterday.year &&
dateTime?.month == yesterday.month &&
dateTime?.day == yesterday.day) {
return "Yesterday";
} else {
return DateFormat.yMMMd().format(dateTime!);
}
}
}
i don't know is there any issue in this library or my code logic , i am new to this. my main aim is the list always at bottom end like the chat app, when we tap on textfield or sending message its always need to be bottom.
chat textfield class code
class TextField extends StatefulWidget {
final Function(String)? onMessageSent;
final Function(XFile)? onImageSelected;
final FocusNode? focusNode;
final TextEditingController messageController;
const TextField({super.key, this.onMessageSent, this.onImageSelected,required this.messageController,
this.focusNode
});
@override
State<TextField> createState() => _TextFieldState();
}
class _TextFieldState extends State<TextField> {
XFile? image;
bool isAttached = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
isAttached
? Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Column(
children: [
if (isAttached) ...[
SizedBox(height: 10.h),
SizedBox(height: 12.h),
],
],
),
)
: const SizedBox.shrink(),
Container(
height: 72.h,
padding: EdgeInsets.only(left: 15.w, right: 18.w),
decoration: const BoxDecoration(
color: Colors.white,
),
child: Row(
children: [
GestureDetector(
onTap: () {
setState(() {
isAttached = !isAttached;
});
},
child: Image.asset(
"assets/images/icon.png",
width: 22.w,
height: 23.h,
color: isAttached
? Theme.primaryColor
: Theme.inactivateColor,
)),
SizedBox(width: 16.w),
Expanded(
child: TextField(
focusNode: widget.focusNode,
maxLines: null,
controller: widget.messageController,
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack),
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: () {
final messageText = widget.messageController.text;
if (messageText.isNotEmpty) {
widget.onMessageSent!(messageText);
widget.messageController.clear();
}
},
child: CircleAvatar(
backgroundImage:
const AssetImage("assets/images/ellipse_gradient.png"),
radius: 22.5.r,
child: Image.asset(
"assets/images/send_icon.png",
height: 24.h,
width: 30.w,
),
)),
],
),
),
],
);
}
Widget _buildAttachOption(String text, ImageSource source) {
return GestureDetector(
onTap: () async {
},
child: Container(
padding: EdgeInsets.only(left: 15.w, top: 13, bottom: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: ThemeManager.primaryWhite,
),
child: Row(
children: [
Image.asset("assets/images/images_attach.png"),
SizedBox(width: 20.w),
Text( text,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w400,
color: Theme.primaryBlack),
),
],
),
),
);
}
}
Message model class code
class Message implements Comparable<Message>{
final String? senderId;
final String? receiverId;
final String? message;
final String? messageType;
final Timestamp timeStamp;
final String? groupId;
Message({
required this.senderId,
required this.receiverId,
required this.message,
required this.messageType,
required this.groupId,
required this.timeStamp,
});
Map<String, dynamic> toMap() {
return {
'senderId': senderId,
'receiverId': receiverId,
'message': message,
'messageType': messageType,
'TimeStamp': timeStamp,
'groupId': groupId,
};
}
// Factory constructor to create a Message instance from a DocumentSnapshot
factory Message.fromFireStore(DocumentSnapshot doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
return Message(
senderId: data['senderId'],
receiverId: data['receiverId'],
message: data['message'],
messageType: data['messageType'],
timeStamp: data['TimeStamp'],
groupId: data['groupId'],
);
}
factory Message.fromMap(Map<String, dynamic> map) {
return Message(
senderId: map['senderId'] as String,
receiverId: map['receiverId'] as String,
message: map['message'] as String,
messageType: map['messageType'] as String,
timeStamp: map['TimeStamp'] as Timestamp,
groupId: map['groupId'] as String,
);
}
@override
String toString() {
return 'Message{senderId: $senderId, receiverId: $receiverId, message: $message, messageType: $messageType, timeStamp: $timeStamp, groupId: $groupId}';
}
@override
int compareTo(Message other) {
return timeStamp.compareTo(other.timeStamp);
} // converted to map
}
i found the solution need to remove the scrollController listener from initstate and also need to remove _scrollToBottom function ,no need to control the textfield also, the list scroll end at the with the help of groupedListview with reverse true option.
Widget build(BuildContext context) {
// WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); - need to remove the code from here.
return Scaffold(
backgroundColor: ThemeManager.scaffoldBackgroundColor,
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ChatScreenAppBar(
senderName: widget.name, avatarUrl: widget.profileUrl),
Expanded(child: _buildMessageList()),
TextField(
onMessageSent: (text) {
sendMessage();
// _scrollToBottom(); - need to remove the code from here also
},
onImageSelected: (selectedImage) async {}, messageController: _messageController,
),
],
),
);
}
no need to use the scrollToBottom function , the GroupedListview libray's reverse true option will help to scroll the list at the bottom.