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();
  }
}

Creating a Flutter chat app with Firebase integration involves setting up Firebase for both the backend services like Firebase Auth for authentication and Firestore for real-time database functionalities. Below is a step-by-step guide to building a basic chat application in Flutter with Firebase.

Part I: Steps To Build Your Chat App In Flutter
In this tutorial, we have demonstrated step-by-step instructions on how to build an Android chat app with flutter & firebase that lets users send and receive messages.

Minimum Requirements

Flutter 2.0.0 or later
Android 4.4 or later
Chat SDKs & License Key
Dart 2.12.0 or later
Firebase Account

Step 1: Get Your Chat SDKs And License Key

  • Create your MirrorFly account
  • Verify your account with the Confirmation link sent to your Email ID
  • Sign in to your Account and complete the account setup process
  • Download the Flutter SDK Package
  • Extract the downloaded SDK files onto your device
  • Again, go to your Account Dashboard
  • Make a note of your License Key

Step 2: Get Started With Your Chat App Project In Android Studio
In this step, you’ll create a new Android project, setting up Flutter as your development language.

Open the Android Studio IDE

  • Fill in the basic information of your project. Your Project Window will open
  • From the Downloads folder of your device, import the dependencies to your project’s App Folder.
  • Next, add the pubspec.yaml file
  • To avoid library conflicts between the imported files, you’ll need to add the below line in the gradle.properties
android.enableJetifier=true

You’ll require permission to access different components of the project. To grant them, add the below lines to the AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Next, in the app/build.gradle configure the License key that you acquired from your MirrorFly Account Dashboard.

plugins {
	...
	id 'kotlin-android'
	id 'kotlin-kapt'
}
android {
	compileOptions {
    	sourceCompatibility JavaVersion.VERSION_1_8
    	targetCompatibility JavaVersion.VERSION_1_8
	}

	kotlinOptions {
    	jvmTarget = '1.8'
	}

	packagingOptions {
    	exclude 'META-INF/AL2.0'
    	exclude 'META-INF/DEPENDENCIES'
    	exclude 'META-INF/LICENSE'
    	exclude 'META-INF/LICENSE.txt'
    	exclude 'META-INF/license.txt'
    	exclude 'META-INF/NOTICE'
    	exclude 'META-INF/NOTICE.txt'
    	exclude 'META-INF/notice.txt'
    	exclude 'META-INF/ASL2.0'
    	exclude 'META-INF/LGPL2.1'
    	exclude("META-INF/*.kotlin_module")
	}
}

Step 3: Initialize The Chat SDKs
In your Android Application Class, go to the onCreate() method and add the ChatSDK builder. This step will help you get all the necessary details to get started with the SDK initialization.

Kotlin:

//For chat logging
LogMessage.enableDebugLogging(BuildConfig.DEBUG)
 
ChatSDK.Builder()
	.setDomainBaseUrl(BuildConfig.SDK_BASE_URL)
	.setLicenseKey(BuildConfig.LICENSE)
	.setIsTrialLicenceKey(true)
	.build()

Step 4: Register a User
Once you’ve initialized the SDK, you’ll need to register the user using the USER_IDENTIFIER and FCM_TOCKEN
You can create the user either in the live or sandbox mode, as given below
Flutter:

FlyChat.registerUser(USER_IDENTIFIER, FCM_TOCKEN).then((value) {
	if (value.contains("data")) {
	// Get Username and password from the object
	}
}).catchError((error) {
	// Register user failed print throwable to find the exception details.  
});

Once you register the user, the server automatically gets connected.

Step 5: Start Sending And Receiving Messages

  1. Send a Chat Message

Add the sendTextMessage() method to start sending messages from your Flutter app.

FlyChat.sendTextMessage(TEXT, TO_JID, replyMessageId).then((value) {
     // you will get the message sent success response
 });

Receive a Chat Message
Get notifications on any incoming message, using the below onMessageReceived() method

FlyChat.onMessageReceived.listen(onMessageReceived);
void onMessageReceived(chatMessage) {
     //called when the new message is received
 }

You’ve now successfully built a chat app in Flutter that can send and receive messages. Now, let us move on to integrate Firebase into your application.

Part II: Steps To Integrate FireBase Into Your App
Follow the below steps to add Firebase services to your messaging application:

Step 1 : Create a Firebase Project

  • Open the Firebase Console
  • Create a Project using the package name of your Flutter application. Let us assume that name is com.testapp

Step 2: Configure Your App Info In The Console

To store and manage your app’s configuration information the Google Developer Console, you’ll need to perform the following steps:

  • Go to Console
  • Select your project
  • Navigate to Project settings
  • Select Cloud Messaging. You’ll find the google-service.json file here
  • Download the file
  • Add it to the App folder of your project

Step 3: Add Firebase Dependencies
Once you’ve set up the configuration, you’ll need to add the below Firebase dependencies. To do this,
Go to build.gradle
Add the below repos

buildscript {
    repositories {
        // Check that you have the following line (if not, add it):
        google()  // Google's Maven repository
   }
   dependencies {
       classpath 'com.google.gms:google-services:4.3.13' // google-services plugin
   }
}

allprojects {
   repositories {
       // Check that you have the following line (if not, add it):
       google() // Google's Maven repository
   }
}

Next, add the plugins for the Android app and Google services

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

dependencies {
  implementation platform('com.google.firebase:firebase-bom:30.3.1')
}

You’ve now successfully set up Firebase for your app. Let’s proceed further to integrate the Push notifications