Mastering Chat Screen Design: Save Chats Locally with Realm Database | Step-by-Step Tutorial Episode [13]
Key Topics covered:
- Bump Realm Schema Version
- Save Chats on local device
- Save Friends List on local device
Description:
In this tutorial, you will learn the art of designing an engaging chat screen and saving chat heads into a local database using Realm. In this tutorial, we'll dive deep into the world of chat interface design and explore how to implement efficient data storage techniques. Whether you're a beginner or an experienced developer, this video will provide valuable insights and practical examples to enhance your app-building skills.
Code Snippet(ChatRealm.dart):
Create _ChatRealm class. A realm class must start with underscore and annotated with @RealmModel()
- Create lib/model/db/ChatRealm.dart
import 'package:realm/realm.dart';
part 'ChatRealm.g.dart';
@RealmModel()
class _ChatRealm {
@PrimaryKey()
late ObjectId id;
late String? senderUserId;
late String? receiverUserId;
late String? message;
late String? type;
late DateTime? sendDate;
late bool? read;
late String? key;
late String? displayName;
late String? base64Image;
late int? count;
}
Code Snippet(FriendContactRealm.dart):
Create _FriendContactRealm class. A realm class must start with underscore and annotated with @RealmModel()
- Create lib/model/db/FriendContactRealm.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:realm/realm.dart';
part 'FriendContactRealm.g.dart';
@RealmModel()
class _FriendContactRealm {
@PrimaryKey()
late ObjectId id;
late String? userId;
late String? mobileNumber;
late String? displayName;
late String? base64Image;
}
Don't forget to run the following command in your terminal to generate ChatRealm.g.dart and FriendContactRealm.g.dart with messagesdart run realm generate
Code Snippet(RealmProvider.dart) modification:
Increase schema version since we are adding new schemas for chats and friends
- lib/core/providers/RealmProvider.dart
final config = Configuration.local([MessageRealm.schema,ChatRealm.schema, FriendContactRealm.schema],schemaVersion:1);
Code Snippet(ChatState.dart):
Create ChatState
- Create lib/model/ChatState.dart
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/db/ChatRealm.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'ChatState.freezed.dart';
@Freezed(makeCollectionsUnmodifiable:false)
class ChatState with _$ChatState {
factory ChatState({
@Default('') String myUserId,
@Default(false) bool loading,
@Default([]) List<ChatRealm> chats,
}) = _ChatState;
}
Code Snippet(FriendState.dart):
Create FriendState
- Create lib/model/FriendState.dart
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/db/ChatRealm.dart';
import 'package:chat_with_bisky/model/db/FriendContactRealm.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'FriendState.freezed.dart';
@Freezed(makeCollectionsUnmodifiable:false)
class FriendState with _$FriendState {
factory FriendState({
@Default('') String myUserId,
@Default(false) bool loading,
@Default([]) List<FriendContactRealm> friends,
}) = _FriendState;
}
Don't forget to run the following command in your terminal to regenerate ChatState.freezed.dart and FriendState.freezed.dart with messagesflutter packages pub run build_runner build
Code Snippet(DashboardPage.dart)modification:
Call ChatListScreen when index is 0 in getSelectedScreen() method. We are going to create the screen soon.
- Update lib/pages/dashboard/DashboardPage.dart
case 0:
return ChatListScreen();
Code Snippet(ChatListViewModel.dart):
- Create lib/pages/dashboard/chat/list/ChatListScreen.dart
import 'package:auto_route/auto_route.dart';
import 'package:chat_with_bisky/model/ChatState.dart';
import 'package:chat_with_bisky/model/db/ChatRealm.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/list/ChatListViewModel.dart';
import 'package:chat_with_bisky/route/app_route/AppRouter.gr.dart';
import 'package:chat_with_bisky/service/LocalStorageService.dart';
import 'package:chat_with_bisky/widget/custom_app_bar.dart';
import 'package:chat_with_bisky/widget/friend_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ChatListScreen extends ConsumerStatefulWidget {
@override
ConsumerState<ChatListScreen> createState() => _ChatListScreenState();
}
class _ChatListScreenState extends ConsumerState<ChatListScreen> {
ChatListViewModel? notifier;
ChatState? model;
@override
Widget build(BuildContext context) {
notifier = ref.read(chatListViewModelProvider.notifier);
model = ref.watch(chatListViewModelProvider);
return Scaffold(
body: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
CustomBarWidget(
"Chats",
),
model?.chats.isNotEmpty == true
? Expanded(
child: ListView.separated(
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
ChatRealm friend = model!.chats[index];
return ListTile(
leading: FriendImage(friend.senderUserId!),
title: Text(friend.displayName ?? ""),
subtitle: Text(friend.message ?? ""),
onTap: () async {
String userId =
await LocalStorageService.getString(
LocalStorageService.userId) ??
"";
AutoRouter.of(context).push(MessageRoute(displayName: friend.displayName ?? "",
myUserId:userId,
friendUserId:friend.senderUserId ?? ""));
},
);
},
separatorBuilder: (context, index) {
return const SizedBox(
width: 1,
);
},
itemCount: model?.chats.length ?? 0))
: const Center(
child: Text(
"You do not have chats. Please invite your loved ones and start chatting"),
)
],
),
),
),
);
}
getChats() async {
String userId =
await LocalStorageService.getString(LocalStorageService.userId) ?? "";
notifier?.getChats(userId);
}
Future<void> initialization() async {
String userId =
await LocalStorageService.getString(LocalStorageService.userId) ?? "";
Future.delayed(
const Duration(seconds: 1),
() {
notifier?.changedUserId(userId);
notifier?.initializeFriends(userId);
getChats();
},
);
}
@override
void initState() {
super.initState();
initialization();
}
}
Code Snippet(ChatListScreen.dart)modification:
ChatMessage widget and messageWidget must accept MessageRealm object from now on.
- Create lib/pages/dashboard/chat/list/ChatListViewModel.dart
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/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/RealmProvider.dart';
import 'package:chat_with_bisky/model/ChatAppwrite.dart';
import 'package:chat_with_bisky/model/ChatState.dart';
import 'package:chat_with_bisky/model/db/ChatRealm.dart';
import 'package:realm/realm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'ChatListViewModel.g.dart';
@riverpod
class ChatListViewModel extends _$ChatListViewModel{
Databases get _databases => ref.read(databaseProvider);
Realm get _realm => ref.read(realmProvider);
@override
ChatState build(){
return ChatState();
}
changedUserId(userId){
state = state.copyWith(myUserId: userId);
}
getChats(String userId) async{
try{
DocumentList documentList = await _databases.listDocuments(databaseId: Strings.databaseId, collectionId: Strings.collectionChatsId,
queries: [
Query.equal("receiverUserId", [userId]),
Query.orderDesc('\$updatedAt'),
Query.limit(100)
]);
if(documentList.total > 0){
for(Document document in documentList.documents){
ChatAppwrite chatAppwrite = ChatAppwrite.fromJson(document.data);
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(document.$updatedAt),
);
createOrUpdateChatHead(chatRealm);
}
initializeFriends(userId);
}
}catch (e){
print(e);
}
}
void createOrUpdateChatHead(ChatRealm chatRealm) {
final results = _realm.query<ChatRealm>(r'senderUserId = $0',[chatRealm.senderUserId??""]);
if(results.isNotEmpty){
ChatRealm retrieved = results.first;
chatRealm.id = retrieved.id;
}
_realm.write(() {
_realm.add(chatRealm,update: true);
});
}
initializeFriends(String userId){
final results = _realm.query<ChatRealm>(r'receiverUserId = $0 SORT(displayName ASC)',[userId]);
if(results.isNotEmpty){
state = state.copyWith(
chats: results.toList()
);
}
}
}
Don't forget to run the following command in your terminal to regenerate ChatListViewModel.g.dart flutter packages pub run build_runner build
Code Snippet(FriendListViewModel.dart):
- Create lib/pages/dashboard/friends/FriendListViewModel.dart
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/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/RealmProvider.dart';
import 'package:chat_with_bisky/model/FriendContact.dart';
import 'package:chat_with_bisky/model/FriendState.dart';
import 'package:chat_with_bisky/model/db/FriendContactRealm.dart';
import 'package:realm/realm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'FriendListViewModel.g.dart';
@riverpod
class FriendListNotifier extends _$FriendListNotifier{
Databases get _databases => ref.read(databaseProvider);
Realm get _realm => ref.read(realmProvider);
@override
FriendState build(){
return FriendState();
}
changedUserId(String userId){
state = state.copyWith(
myUserId: userId
);
}
getMyFriends(String userId) async {
DocumentList documentList = await _databases.listDocuments(databaseId: Strings.databaseId,
collectionId: Strings.collectionContactsId,
queries: [
Query.equal("userId", [userId]),
]);
if(documentList.total > 0){
List<Document> documents =documentList.documents;
for(Document document in documents){
FriendContact friend = FriendContact.fromJson(document.data);
FriendContactRealm friendContactRealm = FriendContactRealm(
ObjectId(),
userId: friend.userId,
mobileNumber: friend.mobileNumber,
displayName: friend.displayName,
);
createOrUpdateFriend(friendContactRealm);
}
initializeFriends(userId);
}
}
createOrUpdateFriend(FriendContactRealm friendContactRealm){
final results = _realm.query<FriendContactRealm>(r'mobileNumber = $0',[friendContactRealm.mobileNumber]);
print(friendContactRealm.userId);
print(friendContactRealm.displayName);
print(friendContactRealm.mobileNumber);
print(results.length);
if(results.isNotEmpty){
FriendContactRealm retrieved = results.first;
friendContactRealm.id = retrieved.id;
}
_realm.write(() {
_realm.add(friendContactRealm,update: true);
});
}
initializeFriends(String userId){
final results = _realm.query<FriendContactRealm>(r'userId = $0 SORT(displayName ASC)',[userId]);
print(results.length);
if(results.isNotEmpty){
state = state.copyWith(
friends: results.toList()
);
}
}
}
Don't forget to run the following command in your terminal to generate FriendListViewModel.g.dart flutter packages pub run build_runner build
Code Snippet(FriendsListScreen.dart):
We need to change FriendsListScreen to extend ConsumerStatefulWidget and also _FriendsListScreenState to extend ConsumerState
- Update lib/pages/dashboard/friends/FriendsListScreen.dart
import 'package:chat_with_bisky/bloc/friend/friend_state.dart' as fBloc;
import 'package:chat_with_bisky/model/FriendState.dart' as frendRiverpodState;
import 'package:chat_with_bisky/model/db/FriendContactRealm.dart';
import 'package:chat_with_bisky/pages/dashboard/friends/FriendListViewModel.dart';
import '../../../bloc/friend/friend_state.dart';
// declare these
FriendListNotifier? notifier;
frendRiverpodState.FriendState? model;
Add below code in build()
//Add below code in build() and BlockListener
notifier = ref.read(friendListNotifierProvider.notifier);
model = ref.watch(friendListNotifierProvider);
// below code in the blocklistener
if (state is ReloadFriendsState){
setState(() {
friends = state.friends;
});
getFriends();
}else if (state is FriendsListState){
getFriends();
}
// replace friends.isNotEmpty with below code
model?.friends.isNotEmpty == true
Refactor itemBuilder(itemCount:) to use FriendContactRealm
FriendContactRealm friend = model!.friends[index];
itemCount: model?.friends.length ?? 0))
Create a method getFriends()
getFriends() async {
if(mounted){
String userId = await LocalStorageService.getString(LocalStorageService.userId) ?? "";
notifier?.getMyFriends(userId);
}
}
Future<void> initialization() async {
String userId = await LocalStorageService.getString(LocalStorageService.userId) ?? "";
Future.delayed(const Duration(seconds: 1),() {
notifier?.changedUserId(userId);
notifier?.initializeFriends(userId);
},);
}
@override
void initState() {
super.initState();
initialization();
}
Conclusion:
We managed to save chats and friends on local device using realm. We added a logic in FriendListViewModel.dart to handle our business logic for getting friends from local device. In the next tutorial, we are going to send chat images. Don't forget to share and join our Discord Channel. May you please subscribe to our YouTube Channel.