androidflutterfirebase-cloud-messagingdart-isolates

How to communicate between FCM background isolate and main isolate


I am building a chat section in my app. I was finishing the handling of data messages sent from the server to the devices when I accidentally stumped upon a serious problem where firebase was opening a new isolate to run background tasks. I use GetX controller ChatController which has a chats variable of type RxList<Chat>. Each chat has a messages list. normally when someone sends a message and the device is in foreground, I take the new message, do some serialization here and there, and add it to its corresponding chat in the ChatController's chats. When doing same thing in background, but it just reinitializes a new one with no data at all which stops the process of adding a new message.

I tried making the chat controller permanent but didn't work. How can I make firebase's background isolate utilize the existing chat controller? even if the user dragged from top to see notifications, I can't update chats. This is really absurd.

if (Get.isRegistered<ChatController>()) {
  // If the controller is already registered, reuse it
  chatController = Get.find<ChatController>();
  print("Chat Controller Found");
} else {
  // If the controller is not registered, initialize it
  chatController = Get.put(ChatController());
  await chatController
      .getChats(); // Only fetch chats on the first initialization
  print("Chat Controller Created!");
}


chatController.addMessageToChat(
    int.parse(message.data["chat_id"]),
    chatMessage,
);

ChatController.dart:

class ChatController extends GetxController with WidgetsBindingObserver {
  final DioClient _dioClient = DioClient();

  RxInt currentChat = (-1).obs;
  RxList<Chat> chats = <Chat>[].obs;

  RxBool isInBackground = false.obs;
  RxList<ChatMessage> backgroundMessages = <ChatMessage>[].obs;
  RxList<ChatMessage> chatMessages = <ChatMessage>[].obs;

  @override
  void onInit() {
    super.onInit();
    getChats();
    WidgetsBinding.instance.addObserver(this); // Add observer for app lifecycle
  }

  @override
  void onClose() {
    WidgetsBinding.instance
        .removeObserver(this); // Remove observer when controller is destroyed
    super.onClose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // App comes back to foreground, refresh data
      isInBackground.value = false;

      for (ChatMessage message in backgroundMessages) {
        print("Message added from backgroundMessages");
        addMessageToChat(message.chatId, message);
      }
      backgroundMessages.clear();
    } else if (state == AppLifecycleState.paused) {
      isInBackground.value = true;
    }
    print("isInBackground from life cycle: ${isInBackground.value}");
  }

  void addMessageToChat(int chatId, ChatMessage message) {
    print("isInBackground from add message: ${isInBackground.value}");
    // if (isInBackground.value) {
    //   backgroundMessages.add(message);
    //   print("Message added to backgroundMessages");
    //   return;
    // } else {
    //   print("Message added to chatMessages");
    // }
    // Find the chat
    Chat chat = chats.firstWhere((chat) => chat.id == chatId);

if (chat.id != -1) {
  chat.messages.add(message);
  Map messageMap = message.toMap();

  messageMap['created_at'] = formatMessageDate(
      message.createdAt.toLocal().toIso8601String(),
      Global.activeUser!.timezone ?? "UTC");
  messageMap['sender']['name'] =
      messageMap['sender']['member_id'] == chat.userId
          ? "You"
          : messageMap['sender']['name'];

  if (messageMap['sender']['member_id'] != chat.userId) {
    chat.counter = currentChat.value != chatId ? chat.counter + 1 : 0;
  }

  chat.lastMessageMap = messageMap;

  // Update the list by reordering the chat without rebuilding the entire list
  int currentIndex = chats.indexOf(chat);
  if (currentIndex != 0) {
    chats.removeAt(currentIndex);
    chats.insert(0, chat);

    chats.refresh(); // This will trigger a partial rebuild
  }
}
 }

Solution

  • What I needed to do was to create another isolate in the main isolate to communicate using ReceiverPort() and SendPort() between FCM isolate and main isolate.

    the new isolate:

    ReceivePort? _receivePort;
    
    void startReceivePort() {
      // Unregister the previous SendPort if it exists
      IsolateNameServer.removePortNameMapping('background_send_port');
    
      _receivePort = ReceivePort();
    
       IsolateNameServer.registerPortWithName(
      _receivePort!.sendPort, 'background_send_port');
    
      _receivePort!.listen((message) async {
        // print("Received message from background isolate: $message");
    
    if (message is Map<String, dynamic>) {
      ChatMessage chatMessage = ChatMessage.fromJson(message['message']);
      String action = message['action'];
    
      if (action == "saveMessage") {
        // Save the message to the database in the main isolate
        await saveMessageToDatabase(chatMessage.chatId, chatMessage);
    
        bool chatExists = await checkIfChatExists(chatMessage.chatId);
    
        if (chatExists) {
          ChatController chatController = Get.find<ChatController>();
          chatController.addMessageToChat(chatMessage.chatId, chatMessage);
    
          int totalUnreadMessagesCount = await getTotalUnreadMessages();
          int totalUnreadChatsCount = await getTotalUnreadChats();
    
          // Push a notification with unread messages data
          NotificationService().pushNotification(
            id: 100,
            body:
                "$totalUnreadMessagesCount unread messages from $totalUnreadChatsCount chats",
            title: "Conversations",
            category: NotificationCategory.Message,
            payload: {
              "screen": "chat",
            },
          );
        }
      }
    }
      });
    }
    
    // Unregister the SendPort when the app terminates to prevent memory 
    leaks
    void stopReceivePort() {
      IsolateNameServer.removePortNameMapping('background_send_port');
      _receivePort?.close(); // Close the ReceivePort
    }
    

    You will need to call startReceivePort() before runApp(const MyApp()) in main.dart and stopReceivePort() in the dispose() in your main stateful widget of your app and voila!