skip to Main Content

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

}

2

Answers


  1. Chosen as BEST ANSWER

    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.


  2. In your build method, you are using WidgetsBinding.instance.addPostFrameCallback(_) to scroll to the bottom of the list when the widget is built.

    @override
    Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
    // rest of the build method 
    }
    

    That means the chat starts at the bottom (where the latest messages are), but it does not handle other interactions like message send or text field focus.
    Since _scrollToBottom is called in multiple places already (like after sending a message), I would remove the automatic scrolling from the build method, to avoid unnecessary scrolling on every frame build.


    Do you think that, in your case, the keyboard’s appearance is causing the scroll issue?

    I think yes because when only touches the textfield it scrolls up.

    When the keyboard appears, it changes the available screen space, which can cause scrollable views to adjust their positions. In a chat application, you typically want the most recent messages to remain visible when the keyboard is opened.

    Try and use the flutter_keyboard_visibility package to listen for keyboard visibility changes. When the keyboard appears, adjust the scroll position to make sure the latest messages stay visible.

    Add flutter_keyboard_visibility to your pubspec.yaml:

    dependencies:
         flutter_keyboard_visibility: ^5.0.2
    

    The Listener would be:

    import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
    
    class _ChatAppState extends State<ChatApp> {
         late StreamSubscription<bool> keyboardSubscription;
    
    @override
    void initState() {
         super.initState();
         keyboardSubscription = KeyboardVisibility.onChange.listen((bool visible) {
              if (visible) _scrollToBottomDelayed();
         });
    }
    
    @override
    void dispose() {
         keyboardSubscription.cancel();
         super.dispose();
    }
    
    void _scrollToBottomDelayed() {
         Future.delayed(Duration(milliseconds: 300), _scrollToBottom);
    }
    
    void _scrollToBottom() {
         if (_scrollController.hasClients) {
              _scrollController.animateTo(
                   _scrollController.position.maxScrollExtent,
                   duration: Duration(milliseconds: 200),
                   curve: Curves.easeOut,
              );
         }
    }
    
    // Rest of your widget code
    }
    

    Make sure the ListView accounts for the keyboard’s presence by adjusting its padding: the last message won’t be hidden behind the keyboard.

    Widget _buildMessageList() {
    return ListView.builder(
         controller: _scrollController,
         reverse: true,
         padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
         itemCount: messages.length,
         itemBuilder: (context, index) {
         // Message item builder code
         },
    );
    }
    

    Make sure the scroll position is adjusted when the TextField gains focus, in case the keyboard visibility listener does not catch it in time.

    final FocusNode _messageFocusNode = FocusNode();
    
    @override
    void initState() {
         super.initState();
         _messageFocusNode.addListener(() {
              if (_messageFocusNode.hasFocus) {
              _scrollToBottomDelayed();
              }
         });
    }
    
    @override
    void dispose() {
         _messageFocusNode.dispose();
         super.dispose();
    }
    
    // Make sure to set the focusNode in your TextField
    TextField(
         focusNode: _messageFocusNode,
         // other TextField properties 
    ),
    

    The OP Nns_ninteyFIve adds in the comments:

    I found the solutions need to remove the scrollController listener from initstate, and also need to remove _scrollToBottom() function, list scroll end at the help of groupedListview with reverse set to true option.

    Removing the ScrollController listener and the _scrollToBottom function while relying on the GroupedListView‘s inherent behavior with reverse set to true is a valid approach, especially for a chat application where the newest messages are typically shown at the bottom.

    • By removing the listener from the ScrollController in the initState, you prevent any additional or unwanted scroll behavior that might have been introduced by manual scroll control.

    • Not using _scrollToBottom means that you are relying on the default scrolling behavior of your list view, which can be more natural and less prone to unexpected behavior, especially in the context of keyboard appearance and focus changes.

    • Setting reverse to true in your GroupedListView automatically handles the scrolling to show the latest messages at the bottom of the list. That is a common practice in chat UIs as it mimics the natural flow of conversation.

    Your _ChatAppState class would be something like:

    class _ChatAppState extends State<ChatApp> {
      final TextEditingController _messageController = TextEditingController();
      // No need for a custom ScrollController
      // final ScrollController _scrollController = ScrollController();
    
      @override
      void dispose() {
        _messageController.dispose();
        // _scrollController.dispose(); // No longer needed
        super.dispose();
      }
    
      // Remove any method that manually handles scrolling like _scrollToBottom
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          // rest of your Scaffold code 
          body: Column(
            children: [
              // other widgets 
              Expanded(child: _buildMessageList()),
              // other widgets 
            ],
          ),
        );
      }
    
      // other methods 
    }
    

    And in the _buildMessageList method, you use GroupedListView with reverse set to true:

    Widget _buildMessageList() {
      // setup for your query and chatRoomId 
    
      return FirestoreQueryBuilder(
        pageSize: 5,
        query: FirebaseFirestore.instance
            .collection('chat_rooms')
            .doc(chatRoomId)
            .collection('messages')
            .orderBy('TimeStamp', descending: true),
        builder: (context, snapshot, _) {
          if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          }
          if (snapshot.isFetching) {
            return const Center(child: CircularProgressIndicator());
          }
    
          List<Message> allMessages = snapshot.docs.map((doc) => Message.fromFireStore(doc)).toList();
    
          return GroupedListView<Message, DateTime>(
            // No need for a custom ScrollController here
            // controller: _scrollController,
            reverse: true, // That keeps your latest messages at the bottom
            elements: allMessages,
            groupBy: (message) => DateTime(
              message.timeStamp.toDate().year,
              message.timeStamp.toDate().month,
              message.timeStamp.toDate().day,
            ),
            // other GroupedListView properties 
          );
        },
      );
    }
    

    By removing the custom scroll logic, that means there is no need for a custom ScrollController or _scrollToBottom method, as GroupedListView takes care of displaying the most recent messages at the bottom.

    The widget structure is simplified, leading to potentially fewer issues related to scroll behavior, especially when interacting with the keyboard or sending messages.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search