flutterdartgoogle-cloud-firestorepaginationfirebaseui

Firebase Firestore database chat app pagination when tap on the message textfield it automatically scrolls up and not scroll down to the end of list


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

}


Solution

  • 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.