Code With Bisky

Effortlessly Sync Existing Contacts in Flutter | Appwrite Integration | Episode 7

In this episode, we dive into the exciting world of contact synchronization. Join us as we explore how to sync existing contacts seamlessly using Flutter and Appwrite.

Key Topics covered:

  • Retrieving and displaying existing contacts
  • Implementing contact synchronization logic
  • Save navigation stage

Description:

The tutorial will walk you through the process reading user contacts and compare with our users registered on our Chat Application. By the end of this tutorial, you'll have a comprehensive understanding of how to sync existing contacts with Flutter and Appwrite, enabling you to build advanced chat applications with ease

Add the following contacts permission in ios/Runner/Info.plist


<key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly</string>
        
        

Code Snippet(FriendContact.dart):

Create a new model called FriendContact.dart. We will use this model to persist the friend in Appwrite database collection. Create a contacts collection on Appwrite dashboard with the same attributes that are in FriendContact.dart

  • lib/model/FriendContact.dart

import 'package:json_annotation/json_annotation.dart';

part 'FriendContact.g.dart';
@JsonSerializable()
class FriendContact{

  String? userId;
  String? mobileNumber;
  String? displayName;

  FriendContact({this.userId, this.mobileNumber, this.displayName});

  factory FriendContact.fromJson(Map<String,dynamic> json)=>
      _$FriendContactFromJson(json);

  Map<String,dynamic> toJson() => _$FriendContactToJson(this);
}
        
        

Don't forget to run the following command in your terminal to generate FriendContact.g.dart flutter packages pub run build_runner build

We need to create friend bloc. Our BLOC pattern will have 3 files which are FriendEvent, FriendState and FriendBloc

  • lib/bloc/friend/friend_bloc.dart
  • lib/bloc/friend/friend_event.dart
  • lib/bloc/friend/friend_state.dart

Code Snippet(FriendEvent.dart):

  • lib/bloc/friend/friend_event.dart

import 'package:equatable/equatable.dart';

abstract class FriendEvent extends Equatable{

  @override
  List<Object?> get props =>[];
}
class ContactsListEvent extends FriendEvent{}
class LoadExistingFriendsEvent extends FriendEvent{}


    

Code Snippet(FriendState.dart):

  • lib/bloc/friend/friend_state.dart

import 'package:chat_with_bisky/model/FriendContact.dart';
import 'package:equatable/equatable.dart';

abstract class FriendState extends Equatable{

  @override
  List<Object?> get props => [];
}

class InitialFriendState extends FriendState{
}

class LoadingRefreshFriendsState extends FriendState{
}

class ReloadFriendsState extends FriendState{
}
class FriendsListState extends FriendState{

  List<FriendContact> friends;
  FriendsListState(this.friends);

  List<Object?> get props => [friends];
}
    
  • Add the following code below in lib/constant/strings.dart. You can update with your own id's from Appwrite


static var avatarImageUrl = 'https://www.w3schools.com/w3images/avatar3.png';
static var collectionContactsId = "6487138a361e8d26179b";

        
        

Code Snippet(FriendBloc.dart):

We will add logic to sync friends in here. The methods name are self-explanatory.

  • lib/bloc/friend/friend_bloc.dart

import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:bloc/bloc.dart';
import 'package:chat_with_bisky/bloc/friend/friend_event.dart';
import 'package:chat_with_bisky/bloc/friend/friend_state.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/model/FriendContact.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/service/AppwriteClient.dart';
import 'package:chat_with_bisky/service/LocalStorageService.dart';
import 'package:contacts_service/contacts_service.dart';
import 'package:flutter/foundation.dart';
import 'package:kiwi/kiwi.dart';
import 'package:uuid/uuid.dart';

class FriendBloc extends Bloc<FriendEvent,FriendState>{
  final AppWriteClientService  appWriteClientService= KiwiContainer().resolve<AppWriteClientService>();
  FriendBloc(FriendState friendState): super(friendState){

    on<ContactsListEvent>((event,emit) async{
      await getContactsListAndPersistFriend(event,emit);
    });
    on<LoadExistingFriendsEvent>((event,emit) async{
      await loadExistingFriends(event,emit);
    });
  }

  getContactsListAndPersistFriend(ContactsListEvent event, Emitter<FriendState> emit) async {
    if(kIsWeb){
      return;
    }

    try{
      List<Contact>  contacts = await ContactsService.getContacts(withThumbnails: false);
      print(contacts.length);
      if(contacts.isNotEmpty){
        emit(LoadingRefreshFriendsState());
        List<String> mobileNumbers = [];
        String userId = await LocalStorageService.getString(LocalStorageService.userId) ?? "";

        Map<String,List<String>> contactsMap = {};
        for(Contact contact in contacts){
          if(contact.phones != null && contact.phones?.isNotEmpty == true){
            for(Item item in contact.phones!){
              String? phone = item.value;
              if(phone != null){
                //+3249 323 343
                //+3249-323-343
                //3249323343
                //049323343
                phone = phone.replaceAll(" ", "");
                if(phone.startsWith("+")){
                  phone = phone.substring(1);
                }else if(phone.startsWith("0")){
                  //01222222222
                  String dialCode = await LocalStorageService.getString(LocalStorageService.dialCode) ?? "";
                  //321222222222
                  phone = "$dialCode${phone.substring(1)}";
                }

                phone = removeSpecialCharacters(phone);
                if(phone.isEmpty){
                  continue;
                }
                if(mobileNumbers.length >= 100){
                  contactsMap[const Uuid().v4()] = mobileNumbers;
                  mobileNumbers = [];
                  mobileNumbers.add(phone);

                }else{
                  mobileNumbers.add(phone);
                }
              }
            }
          }
        }

        if(mobileNumbers.isNotEmpty && mobileNumbers.length < 100){
          contactsMap[const Uuid().v4()] = mobileNumbers;
        }
        if(contactsMap.isNotEmpty){

          contactsMap.forEach((key, value) async {

            Databases databases = Databases(appWriteClientService.getClient());

            print(value);
            DocumentList documentList = await databases.listDocuments(databaseId: Strings.databaseId,
                collectionId: Strings.collectionUsersId,
            queries: [
              Query.equal('userId', value)
            ]);

            if(documentList.total > 0){
              // create friend
              List<FriendContact>  friends = [];
              for(Document document in documentList.documents){
                UserAppwrite user =  UserAppwrite.fromJson(document.data);

                FriendContact contact= FriendContact(
                  mobileNumber: user.userId,
                  displayName: user.name,
                  userId: userId,
                );
                friends.add(contact);
              }
              createOrUpdateMyFriends(friends,userId);

            }
          });

        }
      }

    }catch(exception){
      print(exception);
      emit(ReloadFriendsState());
    }
  }
  String removeSpecialCharacters(String mobileNumber){
    return mobileNumber.replaceAll(RegExp('[^0-9]'), '');
  }

  Future<void> createOrUpdateMyFriends(List<FriendContact> friends,String userId) async {

    Databases databases = Databases(appWriteClientService.getClient());
    for(FriendContact friend in friends){
      try{
        DocumentList documentList = await databases.listDocuments(databaseId: Strings.databaseId, collectionId: Strings.collectionContactsId,
        queries: [
          Query.equal("mobileNumber", [friend.mobileNumber ?? ""]),
          Query.equal("userId", [userId]),
        ]);
        if(documentList.total > 0){

          Document document = documentList.documents.first;
          FriendContact friendContact =  FriendContact.fromJson(document.data);
          friendContact.displayName = friend.displayName;
          Document updatedDocument = await databases.updateDocument(databaseId: Strings.databaseId, collectionId: Strings.collectionContactsId, documentId: document.$id,data: friendContact.toJson());

        print("contact document updated ${updatedDocument.$id}");
        }else{
          Document newDocument = await databases.createDocument(databaseId: Strings.databaseId, collectionId: Strings.collectionContactsId, documentId: const Uuid().v4(), data: friend.toJson());
          print("contact document created ${newDocument.$id}");
        }
      }catch (exception){
        print(exception);
      }
    }
    emit(ReloadFriendsState());
  }

  loadExistingFriends(LoadExistingFriendsEvent event, Emitter<FriendState> emit) async {

    try{
      Databases databases = Databases(appWriteClientService.getClient());
      String userId= await LocalStorageService.getString(LocalStorageService.userId) ?? "";
      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;
        List<FriendContact> friends = [];
        for(Document document in documents){

          FriendContact friend = FriendContact.fromJson(document.data);
          friends.add(friend);
        }
        emit(FriendsListState(friends));
      }
    }catch (exception){
      print(exception);
    }
  }
}
                

Code Snippet(main.dart) modification:

Add FriendBloc

  • lib/main.dart

    BlocProvider<FriendBloc>(
      create: (context) => FriendBloc((InitialFriendState())),
    ),

        
        

Code Snippet(Dashboard.dart) modification:

Create new method getSelectedScreen(int index) to select bottom navigation selected screen


 Widget getSelectedScreen(int index){
    switch (index){
      case 1:
        return FriendsListScreen();
      default:
         return Container();
    }
 }
        
        

Add onTap event on bottomNavigationBar


        onTap: (value) {
          setState(() {
            selectedIndex = value;
          });
        },
        
        

Code Snippet(FriendsListScreen.dart) modification:

Create new class FriendsListScreen.dart


import 'package:chat_with_bisky/bloc/friend/friend_bloc.dart';
import 'package:chat_with_bisky/bloc/friend/friend_state.dart';
import 'package:chat_with_bisky/model/FriendContact.dart';
import 'package:chat_with_bisky/widget/custom_app_bar.dart';
import 'package:chat_with_bisky/widget/friend_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../../bloc/friend/friend_event.dart';

class FriendsListScreen extends StatefulWidget {
  @override
  State<FriendsListScreen> createState() => _FriendsListScreenState();
}

class _FriendsListScreenState extends State<FriendsListScreen> {
  List<FriendContact> friends = [];
  @override
  Widget build(BuildContext context) {
    return BlocListener<FriendBloc, FriendState>(
      listener: (context, state) {

        if (state is ReloadFriendsState){

          BlocProvider.of<FriendBloc>(context).add(LoadExistingFriendsEvent());

        }else if (state is FriendsListState){

         setState(() {
           friends = state.friends;
         });
        }
      },
      child: Scaffold(
        body: SingleChildScrollView(
          child: SizedBox(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width,
            child: Column(
              children: [
                CustomBarWidget(
                  "Friends",
                  actions: Row(
                    children: [
                      if (!kIsWeb)
                        IconButton(
                          icon: Icon(
                            Icons.refresh,
                            color: Colors.green,
                          ),
                          onPressed: () {
                            print("your menu action here to refresh");
                            refreshFriends();
                          },
                        ),

                    ],
                  ),
                ),
                friends.isNotEmpty
                    ? Expanded(
                        child: ListView.separated(
                            padding: EdgeInsets.zero,
                            itemBuilder: (context, index) {
                              return ListTile(
                                leading: FriendImage(friends[index].mobileNumber!),
                                title: Text(friends[index].displayName ?? ""),
                                onTap: () {
                                  print(friends[index].mobileNumber);
                                },
                              );
                            },
                            separatorBuilder: (context, index) {
                              return const SizedBox(
                                width: 1,
                              );
                            },
                            itemCount: friends.length))
                    : const Center(
                  child: Text("You do not have friends. Please invite your loved ones and start chatting"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
  void refreshFriends() {
    BlocProvider.of<FriendBloc>(context).add(ContactsListEvent());
  }
}        
        

Code Snippet(Login.dart) modification:

Add below code before firing and event BlocProvider.of<AuthBloc>(context) .add(MobileNumberLoginEvent(phone)); in method getPhoneNumber(PhoneNumber number). We want to save phone number Dial Code

  • lib/pages/login/Login.dart

 LocalStorageService.putString(LocalStorageService.dialCode, number.dialCode ?? "");
        
        

Create dialCode string constant in LocalStorageService


static String dialCode = "DIAL_CODE";
        
        

Code Snippet(CustomBarWidget.dart) modification:

Add a custom App Bar

  • lib/widget/custom_app_bar.dart

import 'package:flutter/material.dart';

class CustomBarWidget extends StatelessWidget {

  String title;
  bool? showNavigationDrawer;
  Widget? actions;
  CustomBarWidget(this.title,{super.key, this.showNavigationDrawer = false,this.actions});
  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: MediaQuery.of(context).size.height*0.17,
      child: Stack(
        children: <Widget>[
          Container(
            color: Colors.green,
            width: MediaQuery.of(context).size.width,
            height: 100.0,
            child: Center(
              child: Text(
                title,
                style: const TextStyle(color: Colors.white, fontSize: 18.0),
              ),
            ),
          ),
          Positioned(
            top: 80.0,
            left: 0.0,
            right: 0.0,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 20.0),
              child: DecoratedBox(
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(1.0),
                    border: Border.all(
                        color: Colors.grey.withOpacity(0.5), width: 1.0),
                    color: Colors.white),
                child: Row(
                  children: [
                    showNavigationDrawer == true ?
                    IconButton(
                      icon: const Icon(
                        Icons.menu,
                        color: Colors.green,
                      ),
                      onPressed: () {
                      },
                    ):SizedBox(width:10,),
                    const Expanded(
                      child: TextField(
                        decoration: InputDecoration(
                          hintText: "Search",
                        ),
                      ),
                    ),
                    actions != null? actions! : const SizedBox(width: 10,)
                  ],
                ),
              ),
            ),
          )
        ],
      ),
    );
  }
}


        
        

Code Snippet(friend_image.dart) modification:

Create FriendImage widget

  • lib/widget/friend_image.dart

import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/service/AppwriteClient.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';

class FriendImage extends StatelessWidget {
  final String mobileNumber;
  final AppWriteClientService _clientService =
      KiwiContainer().resolve<AppWriteClientService>();

  FriendImage(this.mobileNumber, {super.key});

  @override
  Widget build(BuildContext context) {

    return InkWell(
      child: FutureBuilder(
        builder: (ctx, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // If we got an error
            if (snapshot.hasError) {
              return const CircleAvatar(
                backgroundImage: NetworkImage(Strings.avatarImageUrl),
              );

              // if we got our data
            } else if (snapshot.hasData) {

              // Extracting data from snapshot object
              final data = snapshot.data as Uint8List;
              return CircleAvatar(
                  backgroundImage: MemoryImage(data)
              );
            }
          }

          // Displaying LoadingSpinner to indicate waiting state
          return const CircleAvatar(
            backgroundImage: NetworkImage(Strings.avatarImageUrl),
          );
        },

        // Future that needs to be resolved
        // inorder to display something on the Canvas
        future: getFilePreview(mobileNumber),
      ),
    );
  }

  getFilePreview(String userId) async {
    try {
      Databases databases = Databases(_clientService.getClient());
      Document document = await databases.getDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionUsersId,
          documentId: userId);
      UserAppwrite user = UserAppwrite.fromJson(document.data);

      Storage storage = Storage(_clientService.getClient());
      Uint8List imageBytes = await storage.getFilePreview(
        bucketId: Strings.profilePicturesBucketId,
        fileId: user.profilePictureStorageId ?? "",
      );

      return imageBytes;
    } catch (exception) {
      if (kDebugMode) {
        print(exception);
      }
      return null;
    }
  }
}

        
        

Conclusion:

We managed to sync users contacts and save them in contacts collection. A user can start a conversation. Don't forget to share and join our Discord Channel, May you please subscribe to our YouTube Channel. In the next Tutorial we will walk you through Riverpod State Management. It is easy and better than Bloc pattern. It has less configurations.