Android Kotlin UI Kit

Add an animated typing indicator to your Android app

In this tutorial you will learn how to build an Android group chat application with CometChat [https://cometchat.com/pro] then enable beautiful animated typing indicators.

Milan Vucic • Apr 21, 2020

In this tutorial you will learn how to build an Android group chat application with CometChat then enable beautiful animated typing indicators. When Captain America (for example) starts typing everyone else will see that "Captain America is typing..." When many people are typing the group will see that "Several people are typing" like you see in Slack.

Here is a preview of what you'll build:

Here we are looking at a group chat but you it would be equally easy (if not easier) to add typing indicators to a one-on-one chat. To build this application we'll be using Kotlin but if you're comfortable enough you can use Java.

You can find the complete source code on GitHub.

In order to get the best out of this tutorial, either download the code from GitHub, follow along and examine it, or try writing the whole thing for yourself using steps below and the documentation. Okay let's begin!

Create your free CometChat account

If you are not familiar with CometChat then no worries. In this tutorial I will teach you everything you need to know. First - though - you'll need to create a free account.

Once you've created your account, create your first app called "AwesomeChatApp":

Hold your horses ⚠️🐴!
To follow this tutorial or run the example source code you'll need to create a V1 application.

v2 will be out of beta soon at which point we will update this tutorial.

It will take a few seconds to create and then you'll see an Explore button. Click Explore then navigate to your keys:

Note both your key and ID because you'll need them soon. Alternatively just leave the tab open.

Adding CometChat to your Kotlin app

First, add the repository URL to the project level build.gradle file in the repositoriesblock under the allprojects section:

allprojects {
repositories {
maven {
url "https://dl.bintray.com/cometchat/pro"
}
maven {
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
}
}

Then, add the CometChat SDK to the app level build.gradle file in the dependenciessection:

dependencies {
implementation 'com.cometchat:pro-android-chat-sdk:1.5.+'
}

Then add the below lines to defaultConfig section of the app level gradle file:

defaultConfig {
ndk {
abiFilters "armeabi-v7a", "x86"
}
}

Finally, add the below lines android section of the app level gradle file.

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

Initialize CometChat in the code:

val appID:String="APP_ID"

CometChat.init(this,appID, object : CometChat.CallbackListener<String>() {
override fun onSuccess(p0: String?) {
Log.d(TAG, "Initialization completed successfully")
}

override fun onError(p0: CometChatException?) {
Log.d(TAG, "Initialization failed with exception: " + p0?.message)
}
})

After you’ve successfully done that, let’s jump straight into code and explain what we’re going to be doing. We need to cover several things:

  • Log in

  • Send and receive messages

  • Add typing indicators

  • Listen for incoming typing indicators

Log in

Upon successfully initializing CometChat, we need to create a Log In screen. This is what ours looks like. All we need to do to log in with CometChat is enter one of the 5 predefined usernames that are superhero1, superhero2, superhero3, superhero4 and yes, you've guessed it, superhero5. We’re not going to be covering how to create a new user in this tutorial, but it can be done easily with CometChat API or by using CometChat Dashboard.

After entering one of these users, hit “Log in”.

Let’s check out what’s going on in the code, what you need to do to make the Log in functionality work.

This is the function that gets called when you tap on that button:

private fun attemptLogin() {
loggingInButtonState()
val UID = usernameEditText.text.toString()
CometChat.login(UID, GeneralConstants.API_KEY, object : CometChat.CallbackListener<User>() {
override fun onSuccess(user: User?) {
redirectToSuperGroup()
}

override fun onError(p0: CometChatException?) {
normalButtonState()
Toast.makeText(this@LoginActivity, p0?.message, Toast.LENGTH_SHORT).show()
}
})
}

Let’s explain the loggingInButtonState, that merely sets the button’s text to “Logging in…” and sets to button to be disabled. normalButtonState does exactly the opposite:

private fun loggingInButtonState() {
loginButton.text = getString(R.string.logging_in)
loginButton.isEnabled = false
}
private fun normalButtonState() {
loginButton.text = getString(R.string.log_in)
loginButton.isEnabled = true
}

This was just to make the app look a little bit nicer, because these are asynchronous calls we’re doing, so the user should know something is going on. We could’ve used any other way of showing some progress, like ProgressBar or ProgressDialog or whatever you wish to use in your app.

The logging in process is pretty straight-forward, we just pass the UID (User ID) and the API_KEY that we got in the first few steps of this tutorial. If our login attempt was successful, we’ll get a User object back, which is quite useful for updating some piece of the UI (e.g. profile picture, username or something similar). We don’t have to manually save the logged in user, CometChat does that automatically for us, and we can always easily retrieve them by calling CometChat.getLoggedInUser(). That’ll be used later to determine whether a message is ours or somebody else’s. After successful login, we call redirectToSuperGroup() which does exactly that, redirects the user to the GroupActivity into an already added group that all these predefined users are a part of. We need to first fetch the details of that group in order to have the details we need:

private fun getSuperGroupDetails() {
// We want to automatically join the already existing group with guid 'supergroup'
val GUID:String="supergroup"

CometChat.getGroup(GUID,object :CometChat.CallbackListener<Group>(){
override fun onSuccess(p0: Group?) {
goToGroupScreen(p0!!)
normalButtonState()
}
override fun onError(p0: CometChatException?) {

}
})
}

private fun goToGroupScreen(group: Group) {
val intent = Intent(this, GroupActivity::class.java)
intent.putExtra("group_id", group.guid)
startActivity(intent)
}

The GUID is hardcoded here to have a value of “supergroup”, that’s solely to omit the unnecessary parts of getting a list of groups from this tutorial and focus on the typing indicators. Of course, it’s very easy to do this for multiple groups with CometChat, just get a list of them, and pass GUID. If you wish to see group list and how to create that, check out this tutorial. Once we’re in the group screen, we’re ready to chat!

Send and receive messages

Sending a message is done not via a regular button, but the “send button” IME option that you can add on your EditText like so:

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:imeOptions="actionSend"/>

The difference between these two approaches is that imeOptions are displayed when the keyboard is out, while a send button would always be present on the UI.

Then you need to add the listener to actually trigger our send method:

messageEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
attemptSendMessage()
true
} else {
false
}
}

Let’s check out attemptSendMessage function:

private fun attemptSendMessage() {
// Attempts to send the message to the group by current user
val text = messageEditText.text.toString()
if (!TextUtils.isEmpty(text)) {
messageEditText.text.clear()
val receiverID: String = group!!.guid
val messageType: String = CometChatConstants.MESSAGE_TYPE_TEXT
val receiverType: String = CometChatConstants.RECEIVER_TYPE_GROUP

val textMessage = TextMessage(receiverID, text, messageType, receiverType)

CometChat.sendMessage(textMessage, object : CometChat.CallbackListener<TextMessage>() {
override fun onSuccess(p0: TextMessage?) {
addMessage(p0)
endTyping()
}
override fun onError(p0: CometChatException?) {

}
})
}
}

This method does several small things like checking whether the message is empty, if it sends the message, it clears the text in our EditText, and finally it sends the message. We need to create an object of type TextMessage and pass to it the necessary data. ReceiverType is *RECEIVER_TYPE_GROUP* for us since we are doing the group chat. Same goes for receiverID, that’s our guid or group id. The method for sending a message is again asynchronous, and we get a callback with the message we sent if it succeeds. For now, let’s only examine addMessage method, we’ll get to endTyping shortly, that’s the ‘typing indicator’ bit. We’re still focusing on the chat here.

private fun addMessage(message: TextMessage?) {
noMessagesGroup.visibility = View.GONE
messagesAdapter.addMessage(message)
messagesRecyclerView.smoothScrollToPosition(messagesAdapter.itemCount - 1)
}

It does 3 things:

  • sets the initially seen “noMessagesGroup” to GONE (that’s just a welcome image that you see when there’s no messages in that particular chat)

  • adds the message to the message adapter (most important bit)

  • scrolls to the bottom of the messages

Most of the logic is held in our adapter, so let’s see what it consists of. We have 2 view types, one for our messages and the other for other people’s messages:

override fun getItemViewType(position: Int): Int {
if (isCurrentUserMessage(messages[position])) {
return GeneralConstants.MY_MESSAGE
}
return GeneralConstants.OTHERS_MESSAGE
}
private fun isCurrentUserMessage(message: TextMessage?): Boolean {
val currentUserId = CometChat.getLoggedInUser()?.uid
return currentUserId!! == message?.sender?.uid
}

This is where we’re using CometChat.getLoggedInUser() that we mentioned earlier. Each message comes with a sender which is a User object that we can use to compare to our logged in user. Depending on that comparison, we determine the View Type of some particular message.
Here’s how onCreateViewHolder and onBindViewHolder look:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
if (viewType == GeneralConstants.MY_MESSAGE) {
return MessageViewHolder(LayoutInflater.from(context).inflate(R.layout.my_message_layout, parent, false))
}
return MessageViewHolder(LayoutInflater.from(context).inflate(R.layout.others_message_layout, parent, false))
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
holder.messageTextView.text = messages[position]?.text
holder.senderNameTextView.text = messages[position]?.sender?.name
// Check if the sender is the current user
Glide.with(context).load(GeneralConstants.AVATARS_URL + messages[position]?.sender?.name).into(holder.avatarImageView)
}

First one inflates (Android term for ‘creates’ a view) a different layout for other people’s messages and mine. The layouts are similar but just shifted to one side of the screen or the other, and with a differently colored chat bubble. We’re using Glide library to load the image into the ImageView. You can load the user’s avatar, but we opted to use UI-Avatars for this demo purpose. It just returns images with letters that you pass to it, really useful for some apps. Other than that, it’s just filling up the text and the username of the sender. The sender’s username is hidden for our messages.

Finally, there’s only 1 more thing to see in MessagesAdapter, and that’s addMessage:

fun addMessage(message: TextMessage?) {
messages.add(message)
notifyDataSetChanged()
}

It adds the message to our list at the end, and notifies the RecyclerView that the data had changed so it redraws. That’s how we append incoming messages at the end.

Let’s go back to GroupActivity and just add the listener for incoming messages, and our chat is done! We do that in onResume and remove them in onPause:

override fun onResume() {
super.onResume()
setTypingListener()
// Add the listener to listen for incoming messages in this screen
setIncomingMessageListener()
}
private fun setIncomingMessageListener() {
CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() {

override fun onTextMessageReceived(message: TextMessage?) {
addMessage(message)
}

override fun onMediaMessageReceived(message: MediaMessage?) {

}

})
}
override fun onPause() {
super.onPause()
CometChat.removeMessageListener(listenerID)
CometChat.removeMessageListener(typingListenerId)
}

We’re also adding typingListener in onResume which we’ll focus on in the next section. As for the incomingMessageListener, we just need to call 1 method, pass some id, and tell it what to do when a new message comes, which in our case is simply addMessage. We’re ignoring the MediaMessage since it’s not the in the scope of this tutorial.

That’s it! The chat part is done, we can send and receive the messages. To test this, run the app on 2 or more devices, log in as a few different users and start sending messages.

Add typing indicators

Once we’ve ensured our chat works fine, let’s add typing indicators to it. We first need to notify CometChat when we have started and stopped typing. For this, I used a OnKeyListener on our input EditText and a Handler to send a stopTyping message if we’ve been inactive for more than 5 seconds. This is an arbitrary value that I chose, so if the user does not enter anything for 5+ seconds, we’ll look at that as if he had stopped typing. Let’s see:

messageEditText.setOnKeyListener { _, _, _ ->
startTypingTimer()
true
}
private fun startTypingTimer() {
if (!isTyping) {
startTyping()
}
isTyping = true
typingHandler.removeCallbacks(stopTypingRunnable)
typingHandler.postDelayed(stopTypingRunnable, TYPING_DELAY)
}

private fun startTyping() {
val typingIndicator = TypingIndicator(group!!.guid, CometChatConstants.RECEIVER_TYPE_GROUP)
CometChat.startTyping(typingIndicator)
}

private fun endTyping() {
val typingIndicator = TypingIndicator(group!!.guid, CometChatConstants.RECEIVER_TYPE_GROUP)
CometChat.endTyping(typingIndicator)
isTyping = false
typingHandler.removeCallbacks(stopTypingRunnable)
}

On each key types, we restart our timer, and if we’re not typing already, we call startTyping which will just notify CometChat that we are indeed typing. typingHandler is what we use to call endTyping. That’s one of the 2 ways you can stop typing, either that way or when you send a message. stopTypingRunnable only calls stopTyping (Don’t you just love Kotlin for it’s syntax simplicity):

private val stopTypingRunnable: Runnable = Runnable { endTyping() }

We first remove all callbacks previously added to typingHandler and post a new one each time user enters something. When 5 seconds is out and he hasn’t typed a thing, we notify CometChat that he’s stopped typing. That’s basically it for this part. Only 1 more thing left to do.

Listen for incoming typing indicators

Remember that setTypingListener from onResume, this is what it does:

private fun setTypingListener() {
CometChat.addMessageListener(typingListenerId, object : CometChat.MessageListener() {
override fun onTypingEnded(typingIndicator: TypingIndicator?) {
removeTypingUser(typingIndicator?.sender)
}

override fun onTypingStarted(typingIndicator: TypingIndicator?) {
addTypingUser(typingIndicator?.sender)
}

})
}

We add/remove the user accordingly when the typing has started/ended. This is what these two methods look like:

val currentlyTyping: MutableList<User?> = ArrayList()

private fun addTypingUser(user: User?) {
for (u in currentlyTyping) {
if (u?.uid == user?.uid) {
return
}
}
currentlyTyping.add(user)
notifyTypingChanged()
}
private fun removeTypingUser(user: User?) {
currentlyTyping.removeIf {it?.uid == user?.uid }
notifyTypingChanged()
}

They just check whether the user is already present in a list of users that are currently typing, and add or remove him. They both also call notifyTypingChanged which updates the UI accordingly:

private fun notifyTypingChanged() {
if (currentlyTyping.size == 0) {
typingLayout.visibility = View.INVISIBLE
} else {

messagesRecyclerView.smoothScrollToPosition(messagesAdapter.itemCount - 1)
typingLayout.visibility = View.VISIBLE
formatTypingMessage(currentlyTyping)
}
}

typingLayout is just a piece of the layout that contains the TextView for showing the people who are typing as well as this neat “3 dots jumping up and down” animation, for which I used this awesome 3rd party library. You can create your own too, but it’s just easier to use this one. If nobody is typing, the layout hides, and if people are typing, we simply format the message to resemble that state :

private fun formatTypingMessage(currentlyTyping: MutableList<User?>) {
if (currentlyTyping.size >= TYPING_THRESHOLD_TEXT) {
typingTextView.text = "Several people are typing"
} else {
if (currentlyTyping.size == 1) {
typingTextView.text = currentlyTyping[0]?.name + " is typing"
} else {
typingTextView.text = currentlyTyping[0]?.name + " and " + currentlyTyping[1]?.name + " are typing"
}
}
}

TYPING_THRESHOLD_TEXT = 2 ; this is used to determine when we’ll no longer have each name separately but rather the generic message “Several people are typing”. You can customize this logic, of course. And that’s it, we have finished the typing indicator functionality as well. You should be able to see something like this:

Conclusion

Within an hour or two, we’ve come from an empty Android studio project to a fully functional group chat app with typing indicators. CometChat is quite powerful and has a lot more to offer, we barely even scratched the surface. If you’re interested in seeing more tutorials covering a wide variety of topics such as adding read/delivered receipts, pushing notifications or group user management (kicking/banning users), be sure to check out our other tutorials.

Milan Vucic

CometChat

Milan, Android developer from Serbia 🇷🇸.

Share it with everyone!

Try out CometChat in action

Experience CometChat's messaging with this interactive demo built with CometChat's UI kits and SDKs.