这是来自 firestore 的分页数据代码,这里我使用来自 pub.dev 的名为 firebase_ui_firestore ^1.5.15 的库,分页工作正常,但这里的问题是当我点击文本字段时它会自动向上滚动列表,这里我总是希望列表位于聊天应用程序的底部,但是当我们第一次单击聊天页面时,它会显示列表并在列表底部滚动并按预期工作,但只有当我们单击文本字段时才会出现问题或发送列表向上滚动的任何消息。
这是图书馆的问题吗?或者逻辑错误请帮忙。
我的主要目标是,当我们尝试向某人发送消息或点击键盘或文本字段时,列表始终位于底部。 下面我粘贴了聊天页面的代码。
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!);
}
}
}
我不知道这个库或我的代码逻辑是否有任何问题,我对此很陌生。 我的主要目标是列表始终位于底部,就像聊天应用程序一样,当我们点击文本字段或发送消息时,它始终需要位于底部。
聊天文本字段类代码
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),
),
],
),
),
);
}
}
消息模型类代码
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
}
在您的
build
方法中,您可以在构建小部件时使用 WidgetsBinding.instance.addPostFrameCallback(_)
滚动到列表底部。
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
// rest of the build method
}
这意味着聊天从底部开始(最新消息所在的位置),但它不处理其他交互,例如消息发送或文本字段焦点。
由于
_scrollToBottom
已经在多个地方被调用(比如发送消息后),我将从构建方法中删除自动滚动,以避免在每个框架构建上不必要的滚动。
在您的文本字段小部件(
TextField
类)中,您有onMessageSent
的回调函数。但当文本字段获得焦点或发送消息时,不会直接调用 _scrollToBottom()
。
TextField(
onMessageSent: (text) {
sendMessage();
_scrollToBottom(); // That is good for when a message is sent
},
// rest of the TextField properties
),
_scrollToBottom()
仅在 onMessageSent
中调用,但在文本字段聚焦时不调用。
向
_messageController
中的
initState
添加监听器以对更改做出反应。当文本字段被清除(发布消息发送)时,请确保列表滚动到底部。
@override
void initState() {
super.initState();
_messageController.addListener(_handleMessageChange);
}
void _handleMessageChange() {
if (_messageController.text.isEmpty) {
_scrollToBottom();
}
}