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
}
}
}
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!