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

Gowtham
6 min readOct 15, 2021

--

Our Requirements:

  • User creation with Authentication — to identify a user
  • Contacts query — querying existing chat app users
  • One to one Chat — sending and receiving messages(we’re here)
  • Group chat — group creation, sending and receiving group messages

If you have not read part 1, it is good to read that first where we saw the Authentication and Contact query functionalities and more about Structuring User data in the firestore collection.

One to one chat

This is the section all of you have been waiting for (or so I am imagining… maybe nobody reads this…In which case I’ll just keep talking to myself).

Let’s begin the chat flow from here

one to one chat flow

📝 Things to do

  • Sending a message
  • Receiving a message

i) Sending a message

To create the message, we have a data class called Message.kt . we are only covering text messaging functionality to reduce the complexity of this blog. let’s break down the fields in the message data class.

  • id — random generated string
  • createdAt — message created time in UTC
  • from — sender’s user-id
  • to — receiver’s user-id
  • chatUsers — string array to store sender and receiver user-ids
  • type — content type of the message
  • status — delivery status of message as int.
  • textMessage — contains data of text message
  • deliveryTime — message reached time on opposite user’s device
  • seenTime — seen time by the opposite user
  • chatUserId — opposite chat user’s id used for room database relation. ignored when send to firebase
  • seenTime — seen time by the opposite user
  • chatUserId — opposite chat user’s id used for room database relation. ignored when send to firebase

Step 1: Creating message data
When creating a message we need the correct created time of the message. usually, what would we use is val date=Date() so, this date will contain the current datetime data of the device. but, what if the device’s time is manually changed. in that scenarioDate()will give us the wrong time. to overcome this issue, we are using firebase server timestamp. we have annotated the createdAt field with @ServerTimeStamp so, it will be replaced with server timestamp only if the field is null.

Next, we create a message with the required data. for messageId we generate a random id using the below function.

fun generateId(length: Int= 20): String{ //ex: bwUIoWNCSQvPZh8xaFuz
val alphaNumeric = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return alphaNumeric.shuffled().take(length).joinToString("")
}

Step 2: Adding a message to firestore
After we creating a message object we insert it into the local message table. there is a MessageSender class that handles the insertion of messages in firestore. in that class, we check multiple cases before add the message data in the firestore collection.

Okay, just take a chill pill. I know this thing is getting complicated now. Let’s make this simpler for you folks, with an example.

Assume Peter Parker and MJ having a chat.

From user id: Peter
To used id: MJ

Case 1: Check the chat user documentId in the local database. If it is not null, we use that documentId to send the message.

documentId represents the document that contains two chatusers data. in that document all message are stored inside a subcollection called messages

Case 2: Check the document in the firestore with fromUserId_toUserId combination which is Peter_MJ. if it does exist, we use Peter_MJ to add the new message to firestore and save the fromUserId_toUserId id in chatuser documentId field in local database for the next time usage.

Case 3: Check the document in the firestore with toUserId_fromUserId combination which is MJ_Peter. if it does exist, we use MJ_Peter to add the new message to the firestore and save the toUserId_fromUserId id in chatuser documentId field in local database for the next time usage.

Case 4: If this is the very first message between the people, then there will be no occurrences in the firestore and the local database. So, we create a new document for these users in Messages collection with the documentId fromUserId_toUserId which is none other than Peter_MJ. Once, the document is created successfully, we use the document to add a new message

check the overall code for message send functionality.

firestore after a message added

ii). Receiving messages

In the last section, we saw how to model message data and insert it in the firebase firestore. now, we are going to see how to handle incoming messages. obviously, we are using firestore query snapshot listener to get real-time updates. use the flow diagram to understand each step.

flow of handling received message

Step 1:
We have a class called ChatHandler.kt we have all our code to handle the incoming messages there. as we saw in sending a new message section, we have chatUsers list in Message.kt class that array contains sender and receiver’s user id. chatUsers array going to be our query field. if a message’s chatUsers array contains local-user userId that means, the message is sent by us or sent for us.

messageSubCollection.whereArrayContains("chatUsers", "myUserId")                              .addSnapshotListener { snapShots, error ->                Timber.v("isFromCache ${snapShots?.metadata?.isFromCache}")                    if (error != null || snapShots != null)                    
// handle the message snapshots
}

Whenever a new message document is added or modified(status updated by opposite user) in firestore , the query snapshot listener is triggered. inside the snapShotListener we create a messageList to store the serialized messages.

We use messageDocList to store parent document id.userIds list to store chat user’s id. these two lists will be used in the unRead counting part when we checking for if a message from a new user or not.

If it is from a new user we will fetch the user profile of that user from firestore which is the last process in the flow. let's get back to the snapshot listener, we serialize the message and store it in messageList then, we insert it in the local message table.

setting up snapshot listener and inserting message in local room db

Step 2: Updating unread message count and storing new chat user ids
After the message insertion in Message table. we fetch the chatuser list from local ChatUser table to update the unRead message count and to check it is a message from new user. in newContactIds list new user ids are stored. once, updated the unRead messages count, we update messageDocumentId with messageDocId

We insert the updated chatuser list in the local ChatUser table. so, we can use the updated unread count to show on the home list page.

update chatuser unread message count and message document id

We use the below extension function to find the count of unread message

fun List<Message>.getUnreadCount(oppositeUser: String): Int {
return this.filter { it.from==oppositeUser &&
it.status<3 }.size
}

Step 3: Updating message status (seen or delivered)
Now, it’s time to update the message status. we read all messages from local Message table whichever status is not seen. we have two scenarios here. The first scenario is receiving messages when the app is in the background, the screen went off or the user is on the list screen. in this scenario, we have to update the status to deliveried and have to update the delivery time. in the second scenario, the app user has opened one to one chat screen when receiving a message from the same chat user. in this case, we directly update the message status to seen .

Since we might have to update many documents at a time we can use firestore batch to update the bulk amount of documents.

getAllNotSeenMessage query looks like below which returns all messages except seen

@Query("SELECT * FROM Message  WHERE status<3")
fun getAllNotSeenMessages() : List<Message>
update message status

Step 4: Fetching new chatuser profile
The final step is fetching the new chat user profile and store it with theirunRead message count and messageDocumentId . newContactIds list contains all new users ids. we loop through it and fetch contacts. we store just the mobile number as chat user name, in a real scenario we have to get the contact name from the local phonebook.

new user’s profile fetch and insert new chat user in db

Overall final code for handling the incoming message:

over all code of chathandler.kt

Hope it helped. we have seen the full flow of one-to-one chat.

What’s Next?
We will see the group chat in part 3. checkout the letschat repository to see the full implementation.

Connect with me on GitHub and LinkedIn.

--

--