Flutter Chat MessageLiveCollection Update Example

Hello, I have integrated chat like the code below. What I want to achieve is when sending message/ receiving message by stream I want to add each new message to the existing message objects instead of clearing out the whole message object and repopulating the whole messages object. How can I achieve this? Below is an example code and I believe something needs to be done in the TODO section messageLiveCollection.getStreamController().stream.listen section, also attached video shows the problem when sending message where it refreshes the whole chat messages does making UI/UX not look good. How can I append new messages? Because as of now the messageLiveCollection.getStreamController().stream.listen((event) event currently gives the whole list of amitymessages

Video of how the screen is working(Watch RPReplay_Final1709473407 | Streamable)

import 'package:amity_sdk/amity_sdk.dart';
import 'package:app/constants/app_theme.dart';
import 'package:app/logger.dart';
import 'package:app/services/amity/chat/chat_messaging.dart';
import 'package:app/services/amity/user_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';

class ChatRoom extends StatefulWidget {
  final String channelId;

  const ChatRoom({super.key, required this.channelId});
  @override
  _ChatRoomState createState() => _ChatRoomState();
}

class _ChatRoomState extends State<ChatRoom> {
  final userService = Get.find<UserService>();
  List<types.Message> _messages = [];
  late types.User _user;
  final chatMessaging = Get.find<ChatMessaging>();
  final _messageTextController = TextEditingController();
  late MessageLiveCollection messageLiveCollection;

// Available Message Type options
// AmityMessageDataType.TEXT;
// AmityMessageType.IMAGE;
// AmityMessageType.FILE;
// AmityMessageType.AUDIO;
// AmityMessageType.CUSTOM;

  void listenMessages(String postId) {
    AmitySocialClient.newPostRepository()
        .getPostStream(postId)
        .stream
        .listen((AmityPost post) {
      //handle results
    }).onError((error, stackTrace) {
      //handle error
    });
  }

  void initMessageController() {
    messageLiveCollection = AmityChatClient.newMessageRepository()
        .getMessages(widget.channelId)
        // .stackFromEnd(true)
        .getLiveCollection(pageSize: 20);

    messageLiveCollection.getStreamController().stream.listen((event) {

//TODO I BELIEVE SOMETHING NEEDS TO BE DONE HERE???
      setState(() {
        _messages.clear();

        _messages.addAll(
          event.map(
            (message) => types.TextMessage(
              author: types.User(
                id: message.userId!,
                imageUrl: message.user!.avatarUrl,
                firstName: message.user!.displayName,
              ),
              id: const Uuid().v1(),
              text: (message.data as MessageTextData).text ?? "",
            ),
          ),
        );
      });
    });
  }

  Future<bool> loadMoreMessage({bool isForce = false}) async {
    if (messageLiveCollection.hasNextPage() || isForce) {
      _messages.clear();
      await messageLiveCollection.loadNext();
      return true;
    } else {
      return false;
    }
  }

  @override
  void initState() {
    _user = types.User(
      id: FirebaseAuth.instance.currentUser!.phoneNumber!,
      imageUrl: userService.userData.profileFileUrl,
      firstName: userService.userData.realname,
    );
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      messageLiveCollection.loadNext();
    });
    initMessageController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.primaryBackground,
      appBar: AppBar(
        scrolledUnderElevation: 0,
        title: const Text('Chat Room'),
      ),
      body: GestureDetector(
        onTap: () {
          // Call this method here to hide keyboard whenever you tap outside of the TextField
          FocusScope.of(context).requestFocus(FocusNode());
        },
        child: Chat(
          theme: DefaultChatTheme(
            inputTextDecoration: const InputDecoration(
              border: InputBorder.none,
              contentPadding: EdgeInsets.all(0),
              isCollapsed: true,
              focusedBorder: InputBorder.none,
            ),
            inputTextCursorColor: AppColors.primaryText,
            inputContainerDecoration: BoxDecoration(
              color: AppColors.secondaryBackground,
              borderRadius: BorderRadius.circular(20),
            ),
            inputBackgroundColor: AppColors.secondaryBackground,
            backgroundColor: AppColors.primaryBackground,
          ),
          messages: _messages,
          onSendPressed: _handleSendPressed,
          user: _user,
          onEndReached: loadMoreMessage,
        ),
      ),
    );
  }

  Future<void> _handleSendPressed(types.PartialText message) async {
    await chatMessaging.createMessage(
      widget.channelId,
      message.text,
    );
    _messageTextController.clear();
  }
}

@danielkang98 We would like to inquire if you are utilizing our Flutter UIKit for your project. Could you please inform us whether any customizations have been made to it? Furthermore, it would greatly assist us if you could provide the versions of both Flutter and the UIKit you are currently using.

I am not using amity flutter ui kit. Flutter version

Flutter (Channel stable, 3.16.8, on macOS 14.3.1 23D60 darwin-arm64, locale en-KR)
amity_sdk: ^0.34.0

@danielkang98 For us to investigate the issue you’re encountering, we kindly request additional UI code from you.

Below is the code that I am using for my chat room screen:
Using package flutter_chat_ui: ^1.6.12

import 'package:amity_sdk/amity_sdk.dart';
import 'package:app/constants/app_theme.dart';
import 'package:app/services/amity/chat/chat_channel.dart';
import 'package:app/services/amity/chat/chat_messaging.dart';
import 'package:app/services/amity/chat/edit_chat_room.dart';
import 'package:app/services/amity/user_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';

class ChatRoom extends StatefulWidget {
  final AmityChannel channel;

  const ChatRoom({super.key, required this.channel});
  @override
  _ChatRoomState createState() => _ChatRoomState();
}

class _ChatRoomState extends State<ChatRoom> {
  final chatChannel = Get.find<ChatChannel>();
  final userService = Get.find<UserService>();
  List<types.Message> _messages = [];
  late types.User _user;
  final chatMessaging = Get.find<ChatMessaging>();
  final _messageTextController = TextEditingController();
  late MessageLiveCollection messageLiveCollection;

// Available Message Type options
// AmityMessageDataType.TEXT;
// AmityMessageType.IMAGE;
// AmityMessageType.FILE;
// AmityMessageType.AUDIO;
// AmityMessageType.CUSTOM;

  void listenMessages(String postId) {
    AmitySocialClient.newPostRepository()
        .getPostStream(postId)
        .stream
        .listen((AmityPost post) {
      //handle results
    }).onError((error, stackTrace) {
      //handle error
    });
  }

  void initMessageController() {
    messageLiveCollection = AmityChatClient.newMessageRepository()
        .getMessages(widget.channel.channelId!)
        // .stackFromEnd(true)
        .getLiveCollection(pageSize: 20);

    messageLiveCollection.getStreamController().stream.listen((event) {
      setState(() {
        _messages.clear();

        _messages.addAll(
          event.map(
            (message) => types.TextMessage(
              author: types.User(
                id: message.userId!,
                imageUrl: message.user!.avatarUrl,
                firstName: message.user!.displayName,
              ),
              id: const Uuid().v1(),
              text: (message.data as MessageTextData).text ?? "",
            ),
          ),
        );
      });
    });
  }

  Future<bool> loadMoreMessage({bool isForce = false}) async {
    if (messageLiveCollection.hasNextPage() || isForce) {
      _messages.clear();
      await messageLiveCollection.loadNext();
      return true;
    } else {
      return false;
    }
  }

  @override
  void initState() {
    _user = types.User(
      id: FirebaseAuth.instance.currentUser!.phoneNumber!,
      imageUrl: userService.userData.profileFileUrl,
      firstName: userService.userData.realname,
    );
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      messageLiveCollection.loadNext();
    });
    initMessageController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.primaryBackground,
      appBar: AppBar(
        scrolledUnderElevation: 0,
        title: Text(widget.channel.displayName ?? 'Chat Room'),
        actions: [
          PopupMenuButton<String>(
            color: AppColors.secondaryBackground,
            icon: const Icon(Icons.more_vert),
            onSelected: (String result) {
              // Handle the action based on the selected value
              switch (result) {
                case 'edit':
                  _editChatRoom();
                  // Handle edit action
                  break;
                case 'leave':
                  _leaveChatRoom();
                  break;
                // Add more cases as needed
              }
            },
            itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
              const PopupMenuItem<String>(
                value: 'edit',
                child: Text('Edit Room'),
              ),
              const PopupMenuItem<String>(
                value: 'delete',
                child: Text('Delete Room'),
              ),
              // Add more items as needed
            ],
          ),
        ],
      ),
      body: GestureDetector(
        onTap: () {
          // Call this method here to hide keyboard whenever you tap outside of the TextField
          FocusScope.of(context).requestFocus(FocusNode());
        },
        child: Chat(
          theme: DefaultChatTheme(
            inputTextDecoration: const InputDecoration(
              border: InputBorder.none,
              contentPadding: EdgeInsets.all(0),
              isCollapsed: true,
              focusedBorder: InputBorder.none,
            ),
            inputTextCursorColor: AppColors.primaryText,
            inputContainerDecoration: BoxDecoration(
              color: AppColors.secondaryBackground,
              borderRadius: BorderRadius.circular(20),
            ),
            inputBackgroundColor: AppColors.secondaryBackground,
            backgroundColor: AppColors.primaryBackground,
          ),
          messages: _messages,
          onSendPressed: _handleSendPressed,
          user: _user,
          onEndReached: loadMoreMessage,
        ),
      ),
    );
  }

  Future<void> _handleSendPressed(types.PartialText message) async {
    await chatMessaging.createMessage(
      widget.channel.channelId!,
      message.text,
    );
    _messageTextController.clear();
  }

  void _leaveChatRoom() async {
    await chatChannel.leaveChannel(widget.channel.channelId!);
    Get.back(result: true);
  }

  void _editChatRoom() {
    Get.to(() => EditChatRoom(
          channel: widget.channel,
        ))?.then((value) {
      if (value == true) {
        Get.back(result: true);
      }
    });
  }
}

After the team checked, this seems to be an issue with the state. We would recommend separate the query logic and view by utilizing state management. For instance, you can separate the view from the view model (MVVM).

For more information, please refer to: Flutter MVVM Pattern and Provider State Management | by Madacode | Medium

I have followed the MVVM pattern but still having issue. How are we suppose to handle messages from stream if we are keep on refreshing the messages variable with new message list? I have checked the flutter sample app but it seems wrong as well. May I get help on this? Below video is the error that is occuring. I have create Model View Class and ChatRoomScreen for below code.

(Below link to how the error is occuring)
Error Video

ChatRoomViewModel

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:amity_sdk/amity_sdk.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:uuid/uuid.dart';
import 'package:get/get.dart';
import 'package:app/services/amity/chat/chat_messaging.dart';
import 'package:app/services/amity/user_service.dart';

class ChatRoomViewModel extends ChangeNotifier {
  final ChatMessaging chatMessaging = Get.find<ChatMessaging>();
  final UserService userService = Get.find<UserService>();
  List<types.Message> messages = [];
  late types.User user;
  MessageLiveCollection? messageLiveCollection;

  void initUser() {
    user = types.User(
      id: FirebaseAuth.instance.currentUser!
          .phoneNumber!, // Fetch your user ID appropriately
      imageUrl: userService.userData.profileFileUrl,
      firstName: userService.userData.realname,
    );
  }

  Future<void> loadNext() async {
    messageLiveCollection!.loadNext();
  }

  void listenToMessages(String channelId) {
    messageLiveCollection = AmityChatClient.newMessageRepository()
        .getMessages(channelId)
        .getLiveCollection(pageSize: 20);

    messageLiveCollection!.getStreamController().stream.listen((event) {
      messages.clear();
      messages.addAll(
        event.map((message) => types.TextMessage(
              author: types.User(
                id: message.userId!,
                imageUrl: message.user!.avatarUrl,
                firstName: message.user!.displayName,
              ),
              id: const Uuid().v1(),
              text: (message.data as MessageTextData).text ?? "",
            )),
      );
      notifyListeners(); // This is crucial to update the UI
    });
  }

  Future<void> sendMessage(String channelId, String text) async {
    await chatMessaging.createMessage(channelId, text);
  }
}

ChatRoomScreen

import 'package:amity_sdk/amity_sdk.dart';
import 'package:app/constants/app_theme.dart';
import 'package:app/controllers/chat_view_model.dart';
import 'package:app/logger.dart';
import 'package:app/screens/home/chat/edit_chat_room.dart';
import 'package:app/services/amity/chat/chat_channel.dart';
import 'package:app/services/amity/chat/chat_messaging.dart';
import 'package:app/services/amity/user_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:quickalert/models/quickalert_type.dart';
import 'package:quickalert/widgets/quickalert_dialog.dart';
import 'package:uuid/uuid.dart';

class ChatRoom extends StatefulWidget {
  final AmityChannel channel;

  const ChatRoom({super.key, required this.channel});
  @override
  _ChatRoomState createState() => _ChatRoomState();
}

class _ChatRoomState extends State<ChatRoom> {
  final chatChannel = Get.find<ChatChannel>();
  final userService = Get.find<UserService>();
  List<types.Message> _messages = [];
  final chatMessaging = Get.find<ChatMessaging>();
  final _messageTextController = TextEditingController();

// Available Message Type options
// AmityMessageDataType.TEXT;
// AmityMessageType.IMAGE;
// AmityMessageType.FILE;
// AmityMessageType.AUDIO;
// AmityMessageType.CUSTOM;

  // Future<bool> loadMoreMessage({bool isForce = false}) async {
  //   if (messageLiveCollection.hasNextPage() || isForce) {
  //     _messages.clear();
  //     await messageLiveCollection.loadNext();
  //     return true;
  //   } else {
  //     return false;
  //   }
  // }

  late ChatRoomViewModel _viewModel;

  @override
  void initState() {
    _viewModel = ChatRoomViewModel();
    _viewModel.initUser();
    _viewModel.listenToMessages(widget.channel.channelId!);

    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _viewModel.loadNext();
    });
    super.initState();
  }

  @override
  void dispose() {
    _messageTextController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ChatRoomViewModel>(
      create: (context) => _viewModel,
      child: Consumer<ChatRoomViewModel>(
        builder: (context, viewModel, child) {
          return Scaffold(
            backgroundColor: AppColors.primaryBackground,
            appBar: AppBar(
              scrolledUnderElevation: 0,
              title: Text(widget.channel.displayName ?? 'Chat Room'),
              actions: [
                PopupMenuButton<String>(
                  color: AppColors.secondaryBackground,
                  icon: const Icon(Icons.more_vert),
                  onSelected: (String result) {
                    // Handle the action based on the selected value
                    switch (result) {
                      case 'edit':
                        _editChatRoom();
                        // Handle edit action
                        break;
                      case 'leave':
                        _leaveChatRoom();
                        break;
                      // Add more cases as needed
                    }
                  },
                  itemBuilder: (BuildContext context) =>
                      <PopupMenuEntry<String>>[
                    const PopupMenuItem<String>(
                      value: 'edit',
                      child: Text('Edit Room'),
                    ),
                    const PopupMenuItem<String>(
                      value: 'leave',
                      child: Text('Leave Room'),
                    ),
                    // Add more items as needed
                  ],
                ),
              ],
            ),
            body: GestureDetector(
              onTap: () {
                // Call this method here to hide keyboard whenever you tap outside of the TextField
                FocusScope.of(context).requestFocus(FocusNode());
              },
              child: Chat(
                theme: DefaultChatTheme(
                  inputTextDecoration: const InputDecoration(
                    border: InputBorder.none,
                    contentPadding: EdgeInsets.all(0),
                    isCollapsed: true,
                    focusedBorder: InputBorder.none,
                  ),
                  inputTextCursorColor: AppColors.primaryText,
                  inputContainerDecoration: BoxDecoration(
                    color: AppColors.secondaryBackground,
                    borderRadius: BorderRadius.circular(20),
                  ),
                  inputBackgroundColor: AppColors.secondaryBackground,
                  backgroundColor: AppColors.primaryBackground,
                ),
                messages: _viewModel.messages,
                onSendPressed: _handleSendPressed,
                user: _viewModel.user,
                onEndReached: () {
                  return _viewModel.loadNext();
                },
              ),
            ),
          );
        },
      ),
    );
  }

  Future<void> _handleSendPressed(types.PartialText message) async {
    await chatMessaging.createMessage(
      widget.channel.channelId!,
      message.text,
    );
    _messageTextController.clear();
  }

  void _leaveChatRoom() async {
    try {
      //? If conversation channel, cannot delete
      if (widget.channel.amityChannelType == AmityChannelType.CONVERSATION) {
        Fluttertoast.showToast(
          msg: "You cannot leave 1:1 chat room.",
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.TOP,
          timeInSecForIosWeb: 1,
          backgroundColor: AppColors.accentColor,
          textColor: Colors.white,
          fontSize: 16.0,
        );
      } else {
        await chatChannel.leaveChannel(widget.channel.channelId!);
        AppLogger.logInfo('Left channel: ${widget.channel.channelId}');
        Get.back(result: true);
      }
    } catch (exception) {
      AppLogger.logError('Leave channel failed: $exception');
    }
  }

  void _editChatRoom() {
    Get.to(() => EditChatRoom(
          channel: widget.channel,
        ))?.then((value) {
      if (value == true) {
        Get.back(result: true);
      }
    });
  }
}

@danielkang98 Let me pass this to my team and i’ll back to you soon.

Hello, kindly follow the code sample below.

VIEW:

import 'package:amity_sdk/amity_sdk.dart';
import 'package:amity_uikit_beta_service/viewmodel/chat_room_viewmodel.dart';
import 'package:amity_uikit_beta_service/viewmodel/configuration_viewmodel.dart';
import 'package:animation_wrappers/animations/faded_slide_animation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

class ChatRoomPage extends StatefulWidget {
  final String channelId;
  const ChatRoomPage({
    super.key,
    required this.channelId,
  });

  @override
  State<ChatRoomPage> createState() => _ChatRoomPageState();
}

class _ChatRoomPageState extends State<ChatRoomPage> {
  @override
  void initState() {
    Provider.of<ChatRoomVM>(context, listen: false)
        .initSingleChannel(widget.channelId);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final myAppBar = AppBar(
      automaticallyImplyLeading: false,
      elevation: 0,
      backgroundColor: Provider.of<AmityUIConfiguration>(context)
          .messageRoomConfig
          .backgroundColor,
      leadingWidth: 0,
      title: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          GestureDetector(
              onTap: () {
                Navigator.of(context).pop();
              },
              child: Icon(Icons.chevron_left,
                  color:
                      Provider.of<AmityUIConfiguration>(context).primaryColor,
                  size: 30)),
          Container(
            height: 45,
            margin: const EdgeInsets.symmetric(vertical: 4),
            decoration: const BoxDecoration(shape: BoxShape.circle),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Text(
              Provider.of<ChatRoomVM>(context).channel == null
                  ? ""
                  : Provider.of<ChatRoomVM>(context).channel!.displayName!,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );

    final mediaQuery = MediaQuery.of(context);
    final bHeight = mediaQuery.size.height -
        mediaQuery.padding.top -
        myAppBar.preferredSize.height;
    const textfielHeight = 60.0;
    final theme = Theme.of(context);
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: myAppBar,
      body: SafeArea(
        child: Stack(
          children: [
            FadedSlideAnimation(
              beginOffset: const Offset(0, 0.3),
              endOffset: const Offset(0, 0),
              slideCurve: Curves.linearToEaseOut,
              child: Provider.of<ChatRoomVM>(context).channel == null
                  ? const SizedBox()
                  : SingleChildScrollView(
                      reverse: true,
                      controller:
                          Provider.of<ChatRoomVM>(context).scrollcontroller,
                      child: MessageComponent(
                        bheight: bHeight - textfielHeight,
                        theme: theme,
                        mediaQuery: mediaQuery,
                        channelId: Provider.of<ChatRoomVM>(context)
                            .channel!
                            .channelId!,
                        channel: Provider.of<ChatRoomVM>(context).channel!,
                      ),
                    ),
            ),
            Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                Text("${Provider.of<ChatRoomVM>(context).amitymessage.length}"),
                ChatTextFieldComponent(
                    theme: theme,
                    textfielHeight: textfielHeight,
                    mediaQuery: mediaQuery),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class ChatTextFieldComponent extends StatelessWidget {
  const ChatTextFieldComponent({
    Key? key,
    required this.theme,
    required this.textfielHeight,
    required this.mediaQuery,
  }) : super(key: key);

  final ThemeData theme;
  final double textfielHeight;
  final MediaQueryData mediaQuery;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          color: theme.canvasColor,
          border: Border(top: BorderSide(color: theme.highlightColor))),
      height: textfielHeight,
      width: mediaQuery.size.width,
      padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
      child: Row(
        children: [
          // SizedBox(
          //   width: 5,
          // ),
          // Icon(
          //   Icons.emoji_emotions_outlined,
          //   color: theme.primaryIconTheme.color,
          //   size: 22,
          // ),
          const SizedBox(width: 10),
          SizedBox(
            width: mediaQuery.size.width * 0.7,
            child: TextField(
              controller: Provider.of<ChatRoomVM>(context, listen: false)
                  .textEditingController,
              decoration: const InputDecoration(
                hintText: "Write your message",
                hintStyle: TextStyle(fontSize: 14),
                border: InputBorder.none,
              ),
            ),
          ),
          const Spacer(),
          GestureDetector(
            onTap: () {
              HapticFeedback.heavyImpact();
              Provider.of<ChatRoomVM>(context, listen: false).sendMessage();
            },
            child: Icon(
              Icons.send,
              color: Provider.of<AmityUIConfiguration>(context).primaryColor,
              size: 22,
            ),
          ),
          const SizedBox(
            width: 5,
          ),
        ],
      ),
    );
  }
}

class MessageComponent extends StatelessWidget {
  const MessageComponent({
    Key? key,
    required this.theme,
    required this.mediaQuery,
    required this.channelId,
    required this.bheight,
    required this.channel,
  }) : super(key: key);
  final String channelId;
  final AmityChannel channel;

  final ThemeData theme;

  final MediaQueryData mediaQuery;

  final double bheight;

  String getTimeStamp(AmityMessage msg) {
    if (msg.editedAt == null) {
      return '';
    }
    String hour = msg.editedAt!.hour.toString();
    String minute = "";
    if (msg.editedAt!.minute > 9) {
      minute = msg.editedAt!.minute.toString();
    } else {
      minute = "0${msg.editedAt!.minute}";
    }
    return "$hour:$minute";
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<ChatRoomVM>(builder: (context, vm, _) {
      return Container(
        padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
        child: ListView.builder(
          physics: const NeverScrollableScrollPhysics(),
          shrinkWrap: true,
          itemCount: vm.amitymessage.length,
          itemBuilder: (context, index) {
            var data = vm.amitymessage[index].data;

            bool isSendbyCurrentUser = vm.amitymessage[index].userId !=
                AmityCoreClient.getCurrentUser().userId;
            return Column(
              crossAxisAlignment: isSendbyCurrentUser
                  ? CrossAxisAlignment.start
                  : CrossAxisAlignment.end,
              children: [
                Row(
                  mainAxisAlignment: isSendbyCurrentUser
                      ? MainAxisAlignment.start
                      : MainAxisAlignment.end,
                  children: [
                    if (!isSendbyCurrentUser)
                      Text(
                        getTimeStamp(vm.amitymessage[index]),
                        style: const TextStyle(color: Colors.grey, fontSize: 8),
                      ),
                    vm.amitymessage[index].type != AmityMessageDataType.TEXT
                        ? Container(
                            margin: const EdgeInsets.fromLTRB(10, 4, 10, 4),
                            padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
                            decoration: BoxDecoration(
                                borderRadius: BorderRadius.circular(10),
                                color: Colors.red),
                            child: const Text("Unsupport type message😰",
                                style: TextStyle(color: Colors.white)),
                          )
                        : Flexible(
                            child: Container(
                              constraints: BoxConstraints(
                                  maxWidth: mediaQuery.size.width * 0.7),
                              margin: const EdgeInsets.fromLTRB(10, 4, 10, 4),
                              padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
                              decoration: BoxDecoration(
                                borderRadius: BorderRadius.circular(10),
                                color: isSendbyCurrentUser
                                    ? const Color(0xfff1f1f1)
                                    : Provider.of<AmityUIConfiguration>(context)
                                        .primaryColor,
                              ),
                              child: Text(
                                (vm.amitymessage[index].data!
                                            as MessageTextData)
                                        .text ??
                                    "N/A",
                                style: theme.textTheme.bodyLarge!.copyWith(
                                    fontSize: 14.7,
                                    color: isSendbyCurrentUser
                                        ? Colors.black
                                        : Colors.white),
                              ),
                            ),
                          ),
                    if (isSendbyCurrentUser)
                      Text(
                        getTimeStamp(vm.amitymessage[index]),
                        style: TextStyle(color: Colors.grey[500], fontSize: 8),
                      ),
                  ],
                ),
                if (index + 1 == vm.amitymessage.length)
                  const SizedBox(
                    height: 90,
                  )
              ],
            );
          },
        ),
      );
    });
  }
}

VIEW MODEL:

import 'dart:developer';

import 'package:amity_sdk/amity_sdk.dart';
import 'package:amity_uikit_beta_service/components/alert_dialog.dart';
import 'package:flutter/material.dart';

class ChatRoomVM extends ChangeNotifier {
  AmityChannel? channel;
  TextEditingController textEditingController = TextEditingController();
  final amitymessage = <AmityMessage>[];
  // late PagingController<AmityMessage> messageController;
  final scrollcontroller = ScrollController();
  // Future<void> initSingleChannel(
  //   String channelId,
  // ) async {
  //   await AmityChatClient.newChannelRepository()
  //       .getChannel(channelId)
  //       .then((value) {
  //     channel = value;

  //     notifyListeners();
  //   }).onError((error, stackTrace) async {
  //     log("error from channel");
  //     await AmityDialog().showAlertErrorDialog(
  //         title: "Error!", message: messageController.error.toString());
  //   });

  // Query for message type
  // messageController = PagingController(
  //   pageFuture: (token) => AmityChatClient.newMessageRepository()
  //       .getMessages(channelId)
  //       .getPagingData(token: token, limit: 20),
  //   pageSize: 20,
  // )..addListener(
  //     () async {
  //       if (messageController.error == null) {
  //         print("new update");
  //         amitymessage.clear();
  //         amitymessage.addAll(messageController.loadedItems);
  //         // listenToMessages(channelId);
  //         notifyListeners();
  //       } else {
  //         // Error on pagination controller
  //         log("error from messages");
  //         await AmityDialog().showAlertErrorDialog(
  //             title: "Error!", message: messageController.error.toString());
  //       }
  //     },
  //   );

  // WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  //   messageController.fetchNextPage();
  // });

  // messageController.addListener(loadnextpage);
  // }

  late MessageLiveCollection messageLiveCollection;
  // void listenToMessages(String channelId) {
  //   messageLiveCollection = AmityChatClient.newMessageRepository()
  //       .getMessages(channelId)
  //       .getLiveCollection(pageSize: 20);

  //   messageLiveCollection!.getStreamController().stream.listen((event) {
  //     print("EVENT:${event.length}");
  //     notifyListeners();
  //   });
  // }

  // void loadnextpage() {
  //   if ((scrollcontroller.position.pixels ==
  //           scrollcontroller.position.maxScrollExtent) &&
  //       messageController.hasMoreItems) {
  //     messageController.fetchNextPage();
  //   }
  // }

   Future<void> initSingleChannel(
    String channelId,
  ) async {
    await AmityChatClient.newChannelRepository()
        .getChannel(channelId)
        .then((value) {
      channel = value;

      notifyListeners();
    }).onError((error, stackTrace) async {
      log("error from channel");
      await AmityDialog()
          .showAlertErrorDialog(title: "Error!", message: error.toString());
    });
    messageLiveCollection = AmityChatClient.newMessageRepository()
        .getMessages(channelId)
        .getLiveCollection(pageSize: 20);

    messageLiveCollection.getStreamController().stream.listen((event) {
      print("evemt triggered");
      print("event length: ${event.length}");
      amitymessage.clear();

      amitymessage.addAll(event.reversed);
      notifyListeners();
    });

    messageLiveCollection.loadNext();

    scrollcontroller.addListener(paginationListener);
  }

  void paginationListener() {
    if ((scrollcontroller.position.pixels >=
            (scrollcontroller.position.maxScrollExtent - 100)) &&
        messageLiveCollection.hasNextPage()) {
      messageLiveCollection.loadNext();
    }
  }

  Future<void> sendMessage() async {
    AmityChatClient.newMessageRepository()
        .createMessage(channel!.channelId!)
        .text(textEditingController.text)
        .send()
        .then((value) {
      textEditingController.clear();
    }).onError((error, stackTrace) async {
      // Error on pagination controller
      log("error from send message");
      await AmityDialog()
          .showAlertErrorDialog(title: "Error!", message: error.toString());
    });
  }

  void scrollToBottom() {
    log("scrollToBottom ");
    // scrollController!.animateTo(
    //   1000000,
    //   curve: Curves.easeOut,
    //   duration: const Duration(milliseconds: 500),
    // );
    scrollcontroller.jumpTo(0);
  }

  @override
  Future<void> dispose() async {
    super.dispose();
  }
}