Code With Bisky

Building Real-Time Chat Messaging in Flutter: Background Service for Instant Message Delivery Episode [19]

Key Topics covered:

  • Implement message delivered
  • Show double white ticks
  • Flutter background service
  • Update message delivered to true

Description:

In this tutorial, you will learn how to implement real-time chat message delivery in your Flutter app using the powerful Appwrite backend service. In this tutorial, we'll explore the step-by-step process of building a chat feature that runs seamlessly in the background, ensuring your users receive messages instantly.

Add the following flutter_background_service dependency in your pubspec.yaml. We will configure it to allows the application to mark messages as delivered in background before the user opens the messages


dependencies:
  .........
  flutter_background_service: ^3.0.1
        
        

Code Snippet(MessageRepositoryProvider.dart) modification:

  • lib/core/providers/MessageRepositoryProvider.dart

Create a new method updateMessageDelivered()


  Future<void> updateMessageDelivered(String id) async{

    try{
      _databases.updateDocument(databaseId: Strings.databaseId,
          collectionId: Strings.collectionMessagesId,
          documentId: id,
          data: {
            'delivered':true
          });

    } catch(e){
      print('updateMessageSeen $e');
    }
  }

  // add 'delivered':true for updateMessageSeen() data
      data: {
        'read':true,
        'delivered':true
      });

 
        

Code Snippet(RealmProvider.dart) modification:

  • lib/core/providers/RealmProvider.dart

Let's create realmRepositoryProvider to manage our local data operations on one place. Add below code. We will remove the realm operations from our ViewModels


final realmRepositoryProvider = Provider((ref) => RealmRepositoryProvider(ref));
class RealmRepositoryProvider {
  final Ref _ref;

  get _realm => _ref.read(realmProvider);

  RealmRepositoryProvider(this._ref);

  Future<MessageRealm?> saveMessage(MessageAppwrite messageAppwrite,
      Document document, String type, String myUserId) async {
    if (messageAppwrite.senderUserId != myUserId &&
        messageAppwrite.receiverUserId != myUserId) {
      return null;
    }
    try {
      final message = mapMessageRealm(messageAppwrite, document);
      final results =
      _realm.query<MessageRealm>(r'messageIdUpstream = $0', [message.messageIdUpstream]);
      if (results.isEmpty) {
        _realSaveMessage(message);
        return message;
      } else {
        switch (type) {
          case RealtimeNotifier.create:
          case RealtimeNotifier.update:
            _realSaveMessage(message);
            return message;
          case RealtimeNotifier.delete:
            _realm.delete(message);
            return message;
          default:
            _realSaveMessage(message);
            return message;
        }
      }
    } catch (e) {
      print(e);
      return null;
    }
  }

  Future<ChatRealm?> createOrUpdateChatHead(ChatAppwrite chat,
      Document document, String type, String myUserId) async {
    if (chat.receiverUserId != myUserId) {
      return null;
    }
    ChatRealm chatRealm = mapToChatRealm(chat,document);
    switch (type) {
      case RealtimeNotifier.loading:
      case RealtimeNotifier.create:
      case RealtimeNotifier.update:
        _saveChatRealm(chatRealm);
        return chatRealm;
      case RealtimeNotifier.delete:
        _realm.delete(chatRealm);
        return chatRealm;
      default:
        _saveChatRealm(chatRealm);
        return chatRealm;
    }
  }
  void _saveChatRealm(ChatRealm chatRealm) {
    _realm.write(() {
      _realm.add(chatRealm, update: true);
    });
  }
  void _realSaveMessage(MessageRealm message) {
    _realm.write(() {
      _realm.add(message, update: true);
    });
  }
  MessageRealm mapMessageRealm(MessageAppwrite messageAppwrite,
      Document document){
    DateTime createdDate = DateTime.parse(document.$createdAt);
    final id = ObjectId.fromTimestamp(createdDate);
    return MessageRealm(id,
        senderUserId: messageAppwrite.senderUserId,
        receiverUserId: messageAppwrite.receiverUserId,
        message: messageAppwrite.message,
        type: messageAppwrite.type,
        sendDate: createdDate,
        read: messageAppwrite.read,
        fileName: messageAppwrite.fileName,
        messageIdUpstream: document.$id,
        delivered: messageAppwrite.delivered);
  }

  ChatRealm mapToChatRealm(ChatAppwrite chat,
      Document document){
    final results = _realm.query<ChatRealm>(
        r'senderUserId = $0', [chat.senderUserId]);
    ObjectId id = ObjectId();
    if(results.isNotEmpty){
      id = results.first.id;
    }
    return  ChatRealm(id, senderUserId: chat.senderUserId,
        receiverUserId: chat.receiverUserId,
        message: chat.message,
        type: chat.type,
        sendDate: DateTime.parse(document.$createdAt),
        read: chat.read,
        displayName: chat.displayName,
        count: chat.count,
      userId: chat.userId
    );
  }
}
        

Code Snippet(main.dart):

  • lib/main.dart

We need to add message delivered synchronization in main.dart. We have a one problem here. We want to use the riverpod ref before it is being initiated but I have a solution for that for you. We are going to use ProviderContainer then pass it to UncontrolledProviderScope instead of ProviderScope. Let's initiate all providers that we want to use before initialization. Refactor factor main(0 method to look like this


ProviderContainer _containerGlobal = ProviderContainer();

Future<void> main() async {
  initializeKiwi();
  WidgetsFlutterBinding.ensureInitialized();
  final container = ProviderContainer();
  container.read(appwriteClientProvider);
  container.read(realtimeProvider);
  container.read(realtimeNotifierProvider);
  container.read(realmRepositoryProvider);
  container.read(databaseProvider);
  container.read(storageProvider);
  container.read(messageRepositoryProvider);
  _containerGlobal= container;

  await initializeService();
  runApp(UncontrolledProviderScope(
    container: container,
    child: MultiBlocProvider(providers: [

 
        

Create initializeService() method. We are initializing our background service in here


Future<void> initializeService() async {
  final service = FlutterBackgroundService();
  await service.configure(
    androidConfiguration: AndroidConfiguration(
      // this will be executed when app is in foreground or background in separated isolate
      onStart: onStart,

      // auto start service
      autoStart: true,
      isForegroundMode: true,

      notificationChannelId: 'my_foreground',
      initialNotificationTitle: 'AWESOME SERVICE',
      initialNotificationContent: 'Initializing',
      foregroundServiceNotificationId: 888,
    ),
    iosConfiguration: IosConfiguration(
      // auto start service
      autoStart: true,

      // this will be executed when app is in foreground in separated isolate
      onForeground: onStart,

      // you have to enable background fetch capability on xcode project
      onBackground: onIosBackground,
    ),
  );

  service.startService();
}

        
        

Create _realtimeSynchronisation() that will be listening to the new messages and chats in realtime in background and foreground


_realtimeSynchronisation() async {
  _containerGlobal.listen<RealtimeNotifier?>(
      realtimeNotifierProvider.select((value) => value.asData?.value), (
      previous, next) async {
    if (next?.document.$collectionId == Strings.collectionMessagesId) {
      final message = MessageAppwrite.fromJson(next!.document.data);

      String? userId = await LocalStorageService.getString(LocalStorageService.userId);

      if(userId != null){

        if (message.senderUserId == userId || message.receiverUserId == userId) {

          _containerGlobal.read(realmRepositoryProvider).saveMessage(message, next.document, next.type, userId);

          if(!myMessage(message,userId) && (message.delivered == false || message.delivered == null)){

            _containerGlobal.read(messageRepositoryProvider).updateMessageDelivered(next.document.$id);

          }
        }
      }
    }

    if (next?.document.$collectionId == Strings.collectionChatsId) {
      final chat = ChatAppwrite.fromJson(next!.document.data);
      String? userId = await LocalStorageService.getString(LocalStorageService.userId);
      if(userId != null){
        if (chat.receiverUserId == userId) {

          _containerGlobal.read(realmRepositoryProvider).createOrUpdateChatHead(chat, next.document, next.type, userId);
        }
      }
  });
}

myMessage(MessageAppwrite message, String myUserId){

  return message.senderUserId == myUserId;
}


 

Add below code that are required by our background service


@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
  WidgetsFlutterBinding.ensureInitialized();
  DartPluginRegistrant.ensureInitialized();

  _realtimeSynchronisation();
  return true;
}

@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
  // Only available for flutter 3.0.0 and later
  DartPluginRegistrant.ensureInitialized();
  if (service is AndroidServiceInstance) {
    service.on('setAsForeground').listen((event) {
      service.setAsForegroundService();
    });
    service.on('setAsBackground').listen((event) {
      service.setAsBackgroundService();
    });
  }
  service.on('stopService').listen((event) {
    service.stopSelf();
  });
  _realtimeSynchronisation();
}


 

Check the final class of main.dart here

Code Snippet(MessageViewModel.dart):

  • lib/pages/dashboard/chat/MessageViewModel.dart

Call saveMessage method we created in realmRepositoryProvider


 await ref.read(realmRepositoryProvider)
              .saveMessage(message, document,RealtimeNotifier.loading,state.myUserId);
// make saveMessage async
    saveMessage(MessageAppwrite messageAppwrite, Document document, String type) async {
    ......
// refactor  final message = MessageRealm(...) and use a map from realmRepositoryProvider
    final message = ref
          .read(realmRepositoryProvider)
          .mapMessageRealm(messageAppwrite, document);

    }

    // remove all realm operations in MessageViewModel.dart

The final method of saveMessage should be like this


saveMessage(MessageAppwrite messageAppwrite, Document document, String type) async {
    try {
      final message = ref
          .read(realmRepositoryProvider)
          .mapMessageRealm(messageAppwrite, document);
        switch (type) {
          case RealtimeNotifier.create:
            state = state.copyWith(
              messages: [
                message,
                ...state.messages
              ]
            );

            updateMessageRead(message);
            break;
          case RealtimeNotifier.update:
            state =  state.copyWith(
              messages:  state.messages.map((e) => e.id == message.id ? message:e)
                  .toList()
            );

            updateMessageRead(message);
            break;

          case RealtimeNotifier.delete:
            state =  state.copyWith(
                messages:  state.messages.where((e) => e.id != message.id)
                    .toList()
            );
            break;
          default:
            break;
        }

    } catch (e) {
      print(e);
    }
  }
   

Check the final class of MessageViewModel here

Code Snippet(ChatListViewModel.dart):

  • lib/pages/dashboard/chat/list/ChatListViewModel.dart

On getting chats method. Let's map remove ChatRealm() instantiation and map it. Delete all realm operations from it.



 ChatAppwrite chatAppwrite =  ChatAppwrite.fromJson(document.data);
          await ref
              .read(realmRepositoryProvider)
              .createOrUpdateChatHead(
              chatAppwrite, document, RealtimeNotifier.loading, state.myUserId);



Check the final class of ChatListViewModel here

Code Snippet(ChatMessageItem.dart):

  • lib/widget/ChatMessageItem.dart

Change below code to have double ticks for a read messages


if(myMessage)
  Icon(message.read == true || message.delivered == true?
  Icons.done_all_rounded : Icons.done_rounded,
  size: 16,
  ........


Check the final class of ChatMessageItem here

Conclusion:

We managed to implement message delivered. The chat application can deliver the messages even it is in background. Double white ticks are displayed if the message has been delivered . In the next tutorial, we are going to implement Voice Calls with WebRTC for free with no costs. Don't forget to share and join our Discord Channel. May you please subscribe to our YouTube Channel.