Android — Designing a Chat App with Firebase! (Part 1)

Gowtham
6 min readSep 30, 2021

In November 2020, I started working on a chat app to implement the modern android development tools (Kotlin, Coroutines, Flow, Dagger-Hilt, Architecture Components, MVVM, Room, Testing, Coil, DataStore). In this blog post, I’m going to share my development journey and learnings from this awesome project.

Our Requirements:

User creation with Authentication

To know the identity and securely sign in a user, we use Firebase Phone Auth using the firebase auth identity, we can save our user data.

Hope you all familiar with firebase authentication setup, since the official documentation is pretty clean and our main goal is to focus on structuring data in firestore, will move on. For more details about the authentication part, feel free to check loginRepo.kt for the phone auth integration

Once the user is successfully signed in, we get the unique id of the user from the OnCompleteListener callback response, which is the task object, by using this we can create a document in firestore to store the user data.

Sign in with OTP credential and get the UID

We have UserProfile data class to get the required data of the user.

  • uId — Unique id of the user which we got from authentication
  • createdAt — will be replaced with server timestamp when the value is null
  • updatedAt — to store the last profile updated timestamp. will be replaced with server timestamp same as createdAt field
  • token — FCM token of the user that will be used by other chat users to send message push notifications.
data class for UserProfile

Now, we are all set to add our first user in the firestore. To store all user’s data we create a Collection called Users, in this collection we add a document using the unique id of the user and we can store userProfile data within the document using the below code snippet.

  • uId — unique user id that we got from authentication
  • SetOptions.merge() — If the document does not exist, it will be created. If the document does exist, its contents will be overwritten with the newly provided data.
Once the document is created successfully, the data in firestore will look like this

We are using root level collection structure to structure our data. check structure data documentation to know about other structures in Cloud Firestore

Contacts query

We use user’s device contacts, phone numbers without the country code to query their documents in order to retrieve the user data like image, userName, token. our query works as same as WhatsApp.

i) Creating a dummy contact list

ii) Querying the contacts

iii) Handling the response

i) Creating a dummy contact list

At first, before we initiate the query process, we have to make a mobile number list. For time being, we are using a dummy mobile number list but the actual use case would be fetching the saved contact list from the device.

After creating a dummy contact list with mobile number, we split the elements using a function from the kotlin standard library called chunked which returns List<List<String>>. it splits the elements into chunks with 10 elements each we do this, because of the firestore query limitation and we assign the sublists size to totalQueryCount . totalQueryCount will be helpful to find all queries are completed or not.

In the for loop, 10 contact numbers are passed to makeQuery function each time.

ii) Querying the contacts

There are lots of firebase query operators are available, for our use case WhereIn query operator would be a perfect match. However, it has some limits, it can only query 10 mobile numbers at a time therefore, we have to query multiple times. that’s why we split the mobile number list using chunked function in the first place.

ContactsQuery is a utils class, contains code for the firestore query. we are asynchronously calling query API in order to reduce the query time. QueryListener,queriedList,currentQueryCount and totalQueryCount help us to achieve this.

whenever the onSuccess method is called we serialize the document snapshots to the UserProfile data class and add them in the queriedList . once every query is completed we pass it to onCompleted callback method.

  • queriedList — to store the all queried contacts
  • currentQueryCount — to store the queried count. it is incremented by 1, whenever onSuccess or onFailure called.
  • totalQueryCount — helps us to know that all query is completed.
  • QueryListener — is an interface that has onStart and onCompleted callback methods. onStart is triggered when a new query starts. onCompleted is triggered when curentQueryCount == totalQueryCount which means all queries have got results.

For the first try, the query won’t work. because, we are quering a field(mobile.number) inside a map and composite index is not enabled for this field, we can easily fix it with the firebase error link that we get from the logcat when we run your app. always keep it in mind, while working on firebase queries!

the composite index for contact will query look this

iii) Handling the result

After the query completion onCompleted is triggered and the list of userProfile is passed. we have a kotlin data class called ChatUser by using it we store the other chat users detail in the room database. take a look at our room db tables, will see the usage of each table's in the upcoming section.

all used room tables
  • id — document id of the chatuser
  • localName — locally saved name
  • messageDocId — firestore document id of messages between local user and chat user
  • unRead — Unread messages count

in onCompleted callback we do some operation before we inside user profile in ChatUser table.

Step 1: using fetchLocalDeviceDetails() we fetch the contact list from the local device and map it to Contact data class that we have created. then, we assign the list to localContactContacts variable.

Step 2: We declare finalList which is a list of ChatUser , inside the coroutine scope we get all chat users from ChatUser table to update their new profile, locally saved name.

Step 3: in the for loop, savedNumber variable holds the current element’s locally saved contact name. next, we pass the saved contact name to getChatUser() function.

Step 4: getChatUser() function returns a new chatuser if it does not exist in ChatUser table else it just updates user profile and local saved name to prevent unRead count.

return existChatUser?.apply {           
user = doc
localName = savedName
locallySaved=true } ?: ChatUser(id = doc.uId.toString(),
localName = savedName,
user = doc,
locallySaved = true)

Step 5: We add the updated chat user in finalList. for the final step, insert the finalList in ChatUser table. onwards to setDefaultsfunction we set the default values for the next time usage.

That’s it for this part!👨‍🔧

we will see one-to-one chat in part 2. checkout letschat repository to see the full implementation and reach me out on twitter anytime.

any suggestion/issue? fill in the comment section✍

Thanks to Jaison Fernando for reviewing and making this blog better.

--

--