The Ultimate Live Chat Experience: POLISH UP Synchronizes for Maximum Engagement! Episode [15]
Key Topics covered:
- Create RealtimeNotifierProvider
- Add RealtimeNotifier events
- Refactor MessageScreen by removing the realtime from it
- Listens Realtime events in the MessageViewModel
- Implement Chats to listen to the realtime events
Description:
In this tutorial, we'll learn how to develop a Flutter chat application with real-time messaging and locally saved chat messages for enhanced user experience. In this comprehensive tutorial, we dive into the world of Flutter and explore the process of building a powerful chat app that not only facilitates real-time communication but also ensures message persistence on your local device.
Code Snippet(RealtimeNotifierProvider.dart):
- lib/core/providers/RealtimeNotifierProvider.dart
import 'dart:async';
import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/AppwriteClientProvider.dart';
import 'package:chat_with_bisky/core/providers/RealtimeProvider.dart';
import 'package:chat_with_bisky/model/RealtimeNotifier.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final realtimeNotifierProvider= StreamProvider<RealtimeNotifier>((ref) {
final realtime = ref.read(realtimeProvider);
final controller = StreamController<RealtimeNotifier>();
const channels =[
'databases.${Strings.databaseId}.collections.${Strings.collectionChatsId}.documents',
'databases.${Strings.databaseId}.collections.${Strings.collectionMessagesId}.documents',
];
final subscription = realtime.subscribe(channels).stream.listen((message) {
for(var channel in channels){
final filterEvents = message.events.where((element) => element.contains(channel));
if(filterEvents.isNotEmpty){
final event = filterEvents.first;
final type = event.split('.').last;
if([
RealtimeNotifier.delete,
RealtimeNotifier.create,
RealtimeNotifier.update
].contains(type)){
final document = Document.fromMap(message.payload);
controller.add(RealtimeNotifier(type, document));
}
}
}
});
ref.onDispose(() {
subscription.cancel();
});
return controller.stream;
});
Code Snippet(RealtimeNotifier.dart):
- lib/model/RealtimeNotifier.dart
It has Document and events fired. The real-time will be using this class.
import 'package:appwrite/models.dart';
class RealtimeNotifier{
final String type;
final Document document;
RealtimeNotifier(this.type, this.document);
static const create = "create";
static const delete = "delete";
static const update = "update";
static const loading = "loading";
}
Code Snippet(MessageScreen.dart) modification:
Remove all Real-time logic from this class [listenRealTimeMessages()] and its call. Add below code
- lib/pages/dashboard/chat/MessageScreen.dart
@override
void setState(VoidCallback fn) {
if(mounted){
super.setState(fn);
}
}
}
Code Snippet(MessageViewModel.dart) modification:
Listens to messages in realtime
- lib/pages/dashboard/chat/MessageViewModel.dart
// add below methods
_realtimeSynchronisation() {
ref.listen<RealtimeNotifier?>(
realtimeNotifierProvider.select((value) => value.asData?.value), (
previous, next) {
if (next?.document.$collectionId == Strings.collectionMessagesId) {
final message = MessageAppwrite.fromJson(next!.document.data);
if ((message.senderUserId == state.myUserId ||
message.receiverUserId == state.myUserId) &&
(message.senderUserId == state.friendUserId ||
message.receiverUserId == state.friendUserId)) {
saveMessage(message, next.document, next.type);
}
}
});
}
String getFileExtension(String filePath) {
try {
return '.${filePath
.split('.')
.last}';
} catch (e) {
return "";
}
}
// save message is suppose to have 3 parameters. type is the third one
saveMessage(MessageAppwrite messageAppwrite, Document document, String type) {
}
Let's change the logic in saveMessage from if(results.isEmpty) with the code below
if (results.isEmpty) {
_realm.write(() {
_realm.add(message, update: true);
});
state.messages.insert(0, message);
state = state.copyWith(messages: state.messages);
} else {
switch (type) {
case RealtimeNotifier.create:
state = state.copyWith(
messages: [
message,
...state.messages
]
);
_realm.write(() {
_realm.add(message, update: true);
});
break;
case RealtimeNotifier.update:
state = state.copyWith(
messages: state.messages.map((e) => e.id == message.id ? message:e)
.toList()
);
_realm.write(() {
_realm.add(message, update: true);
});
break;
case RealtimeNotifier.delete:
state = state.copyWith(
messages: state.messages.where((e) => e.id != message.id)
.toList()
);
_realm.write(() {
_realm.delete(message);
});
break;
default:
break;
}
}
// modify save messages in getMessages to have a type "loading" as the 3 parameter
saveMessage(message, document,"loading");
Call _realtimeSynchronisation() in build
@override
MessageState build() {
_realtimeSynchronisation(); // add this
return MessageState();
}
Code Snippet(ChatListScreen.dart) modification:
Add below code
- lib/pages/dashboard/chat/list/ChatListScreen.dart
@override
void setState(VoidCallback fn) {
if(mounted){
super.setState(fn);
}
}
Code Snippet(ChatListViewModel.dart) modification:
Listen chats in real-time
- lib/pages/dashboard/chat/list/ChatListViewModel.dart
// modify createOrUpdateChatHead to have type loading
void createOrUpdateChatHead(ChatRealm chatRealm,String type){...}
// in getChats the type will be loading by default
createOrUpdateChatHead(chatRealm,RealtimeNotifier.loading);
In createOrUpdateChatHead() method let's modify from if(results.isEmpty){
if(results.isEmpty){
_realm.write(() {
_realm.add(chatRealm,update: true);
});
}else{
switch (type) {
case RealtimeNotifier.create:
state = state.copyWith(
chats: [
chatRealm,
...state.chats
]
);
_realm.write(() {
_realm.add(chatRealm, update: true);
});
break;
case RealtimeNotifier.update:
chatRealm.id = results.first.id;
state = state.copyWith(
chats: state.chats.map((e) => e.id == chatRealm.id ? chatRealm:e)
.toList()
);
_realm.write(() {
_realm.add(chatRealm, update: true);
});
break;
case RealtimeNotifier.delete:
state = state.copyWith(
chats: state.chats.where((e) => e.id != chatRealm.id)
.toList()
);
_realm.write(() {
_realm.delete(chatRealm);
});
break;
default:
break;
}
Add chats Real-time listener
_realtimeSynchronisation() {
ref.listen<RealtimeNotifier?>(
realtimeNotifierProvider.select((value) => value.asData?.value), (
previous, next) {
if (next?.document.$collectionId == Strings.collectionChatsId) {
final chatAppwrite = ChatAppwrite.fromJson(next!.document.data);
if (chatAppwrite.receiverUserId == state.myUserId) {
ChatRealm chatRealm = ChatRealm(ObjectId(),
senderUserId: chatAppwrite.senderUserId,
receiverUserId: chatAppwrite.receiverUserId,
type: chatAppwrite.type,
message: chatAppwrite.message,
displayName: chatAppwrite.displayName,
count: chatAppwrite.count,
read: chatAppwrite.read,
sendDate: DateTime.parse(next.document.$updatedAt),
);
createOrUpdateChatHead(chatRealm,next.type);
}
}
});
}
Call _realtimeSynchronisation() in build
@override
ChatState build() {
ref.keepAlive();
_realtimeSynchronisation(); // add this
return ChatState();
}
Conclusion:
We managed to refactor our Real-time implementation by selecting a proper collection Id. Riverpod is managing realtimeNotifierProvider to listens to the changes happens in the collection we added. In the next tutorial, we are going to add video sharing in realtime. Don't forget to share and join our Discord Channel. May you please subscribe to our YouTube Channel.