Code With Bisky

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.