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.