Skip to content

Instant Messaging and Chatrooms on Android with Pusher Chatkit

Instant messaging is undoubtedly one of the most widely used features of any app that users all across the world make use of. When I make a mention of instant messaging, what probably comes immediately to mind is Facebook Messenger, Snapchat, or even the one variant of the million SMS apps and bootlegs that happens to be installed in your phone.

But instant messaging goes just a little bit further, as it’s deployed not only in one-to-one conversational apps, or not even just in chatrooms and collaborative systems like Slack and Discord, but even in the world of customer services when we think about livechat, chatbots and the like.

For whichever use case you have, instant messaging is a feature that many developers should be able to deploy at a good rate, whatever the use case may be. I’m going to show you how to do exactly that with Pusher Chatkit.

Why Pusher Chatkit?

There are quite a few instant messaging tools out there that can be easily deployed in Android. Other ones include Sendbird, and even Firebase (through the use of Firestore and Firebase Cloud Messaging).

I chose Pusher Chatkit purely for its ease of use, and its flexibility to integrate with other services like Dialogflow for example if you wanted to make a chatbot.

A little introduction to Pusher Chatkit

Pusher is a group with a strong reputation in building realtime developer APIs. Pusher is their API for realtime in-app chat and includes SDKs for Android, iOS, JavaScript, and even React, and that’s just their front-end SDKs. They also have SDKs for the back-end, as well as an API interface.

Chatkit works through users joining a room with a given ID, and being able to subscribe to those rooms where they can receive existing messages in that room, as well as any new ones almost immediately after they are sent.

It has a strong reputation and is used by many big apps and companies such as Monzo, Duolingo, and even Github, just to name a few.

Other Chatkit features include Push Notifications, Webhooks, and Indicators for read, typing, and online presence.

More information can be found on their official website.

The App we’ll be making

This is a simple app that contains 2 activities. The LobbyActivity simply has a text field where we enter a room number to join. The MainActivity is basically the chatroom where the user subscribes to receive messages from Chatkit, and also where the user can send messages.

       

The app will be using an MVVM architecture and of course, I’ll be making use of LiveData rather than RxJava here. As with most things, a production app in this case would be better made with dependency injection (Dagger), but for the sake of simplicity of simplicity, I’m not using it for this tutorial.

Accessing the Console

Chatkit’s developer area provides us with some very useful information including an Analytics dashboard, and a console where we can view our users, rooms, and messages which will help us as we go along.

It’s also worth noting that for security purposes, there isn’t a straightforward way the Android SDK allows us to create or delete users. You don’t want normally want to give your users such power. These operations are normally done on the backend and therefore, are available on the backend SDKs of Chatkit. It is possible to do it via the API interface, but for this tutorial, we’ll be creating our users manually in the console.

So go to the Pusher Console and create a new user with an ID and name of your choosing. Take a note of the ID. You’ll need that later.

Also go to the Credentials tab of the console and make a note of the Instance Locator, and the Token Endpoint. You’ll need these in your app.

Import the Dependencies

Finally, we go into the code. Add these dependencies to your app/build.gradle file.

implementation 'com.pusher:pusher-java-client:2.0.1'
implementation 'com.pusher:chatkit-android:1.8.1'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'android.arch.lifecycle:extensions:1.1.1'

As you can tell, on top of the Pusher Chatkit dependencies, we’re also importing RecyclerView and Lifecycle Extensions.

Creating the Repository

Create a Repository class where you handle all your calls to Chatkit. In this class, create a few MutableLiveDatas, then get an instance of ChatManager.

class ChatRepository(context: Context, val roomId: String?) {

    constructor(context: Context): this(context, null)

    var room = MutableLiveData<Room>()
    var chatkitUser = MutableLiveData<CurrentUser>()
    var messages = MutableLiveData<List<Message>>(emptyList())

    private val chatManager = ChatManager(
        instanceLocator = ApiConstants.CHATKIT_INSTANCE_LOCATOR,
        userId = "john,
                dependencies = AndroidChatkitDependencies(
                tokenProvider = ChatkitTokenProvider(
                    endpoint = ApiConstants.CHATKIT_TOKEN_ENDPOINT,
                    userId = "john"
                ),
        context = context
    ))

The class has two constructors. In the LobbyActivitty, we’ll call the constructor without the RoomId because this activity will handle joining or creating the rooms. The RoomId will only be needed when we send and receive messages in the MainActivity.

In the userId fields here, I just hard-coded “john” which I defined earlier in the console to keep this tutorial simple, otherwise this could get easily convoluted.

I’m going to mention now that all of these functions are going to make asynchronous calls, but if you want to make things synchronous instead, just create an instance of SynchronousChatManager instead of the standard ChatManager.

I made the mistake of building the whole Repository with the regular asynchronous ChatManager before I found out about the SynchronousChatManager. I would have preferred the synchronous approach as that would have allowed me to delegate the logic to the ViewModel, rather than handling it in the Repository.

We have 3 pieces of LiveData we need to populate: ChatkitUser, Room, and Messages. Let’s go about fetching their respective data, one by one.

First let’s start with the ChatkitUser.

private fun connectChatkit() {
    chatManager.connect { result ->
        when (result) {
            is Result.Success -> {
                this.chatkitUser.postValue(result.value)
                if (roomId != null) {
                    subscribeToRoom(result.value)
                }
            }

            is Result.Failure -> handleError(result.error)
        }
    }
}

We get the user by simply calling chatManager.connect(). When this method succeeds, we can post that value to the LiveData then subscribe to our room to get our messages, but before we do that, let’s create rooms first.

Joining/Creating Rooms

We have a few situations here we need to work around. We can’t join a room that doesn’t exist, nor can we create a room that’s already been created.

fun attemptJoinRoom(roomId: String) {
    this.chatkitUser.value?.getJoinableRooms { result ->
        when (result) {
            is Result.Success -> {
                for (joinableRoom in result.value)
                    if (joinableRoom.id == roomId) {
                        joinRoom(roomId)
                        return@getJoinableRooms
                    }

                // If room doesn't exist, create it instead
                createRoom(roomId)
            }

            is Result.Failure -> handleError(result.error)
        }
    }
}

The workaround is we can get a list of the rooms a user can join and use simple logic to see if our constructor’s roomId belongs to that list.

fun joinRoom(roomId: String) {
    this.chatkitUser.value?.joinRoom(
        roomId
    ) { result ->
        when (result) {
            is Result.Success -> this.room.postValue(result.value)
            is Result.Failure -> handleError(result.error)
        }
    }
}

fun createRoom(roomId: String) {
    this.chatkitUser.value?.createRoom(
        roomId,
        roomId,
        null,
        false,
        null
    ) { result ->
        when (result) {
            is Result.Success -> joinRoom(roomId)
            is Result.Failure -> handleError(result.error)
        }
    }
}

fun leaveRoom() {
    this.chatkitUser.value?.leaveRoom(
        roomId!!
    ) { result ->
        when (result) {
            is Result.Success -> room.postValue(null)
            is Result.Failure -> handleError(result.error)
        }
    }
}

Finally, we have our 3 functions for Creating, Joining, and Leaving rooms. Each one simply updates our room LiveData accordingly. We can let the ViewModel and Observers handle the rest.

Sending / Receiving Messages

Ok the room stuff was a bit long, but thankfully, sending and messages is a lot simpler.

private fun subscribeToRoom(user: CurrentUser) {
    user.subscribeToRoomMultipart(
        this.roomId!!,
        RoomListeners(onMultipartMessage = { message ->
            this.messages.postValue(Helper.addListElement(this.messages.value!!, message))
        }),
        20
    ) {}
}

fun sendMessage(message: String) {
    this.chatkitUser.value?.sendSimpleMessage(
        this.roomId!!,
        message
    ) { result ->
        when (result) {
            is Result.Failure -> handleError(result.error)
        }
    }
}

Recall, we call subscribeToRoom() when we successfully connect to our ChatManager. Subscribing to a room will give us a previous a number of messages in the room, and every message thereafter almost immediately after Chatkit receives it which we’ll be using to populate our LiveData. That 20 parameter is that number of messages we’ll receive.

SendMessage() is also as straightforward as it gets. After it sends the message, we don’t necessarily need to do anything if it succeeds. The message will be received by Chatkit which will send it back downstream to our room subscription.

fun <T: Any> addListElement(list: List<T>, element: T): List<T> {
    val arrayList = ArrayList(list)
    arrayList.add(element)
    return arrayList.toList()
}

If you wanted it, here’s the Helper function I’m using to add elements to the list. I like keeping my Kotlin safe and keeping my LiveData immutable after it leaves the repository (despite my excessive use of !! operators here. I’m working on it).

Terminating Chatkit

fun terminate() {
    chatManager.close{}
}

When the ChatRepository is no longer used, we have to close ChatManager as well to prevent data leaks and manage garbage and all that. We’ll tie this function to the ViewModel’s own termination function.

Creating the Lobby

As mentioned above, we’ll have 2 activities so that means 2 ViewModels as well. Let’s start with the Lobby and its ViewModel.

LobbyViewModel

class LobbyViewModel: ViewModel() {

    private lateinit var chatRepository: ChatRepository

    fun initChatbotRepository(context: Context) {
        chatRepository = ChatRepository(context)
    }
    
    fun getRoom(): LiveData<Room> {
        return chatRepository.room
    }
    
    fun joinRoom(roomId: String) {
        chatRepository.attemptJoinRoom(roomId)
    }

    override fun onCleared() {
        super.onCleared()
        if (this::chatRepository.isInitialized)
            chatRepository.terminate()
    }

}

I’m keeping things simple here. The LobbyActivity is only concerned with joining rooms, so we have our joinRoom() function to respond to a button click, getRoom() to return LiveData from the Repository, and the termination function for the ChatManager.

I decided to pass Context into the single method of initChatbotRepository() instead of the ViewModel’s constructor because I didn’t want to tie the context to the ViewModel if we were only using it for one function.

LobbyActivity

Let’s start with some copy-pastable XML code.

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".activities.lobby.LobbyActivity">

    <data>
        <variable
            name="lobbyViewModel"
            type="com.example.pusherbot.activities.lobby.LobbyViewModel" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="32dp"
            android:layout_centerInParent="true">

            <EditText
                android:id="@+id/input_roomid"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/white_with_border"
                android:textSize="20sp"
                android:cursorVisible="false"
                android:padding="16dp"
                android:inputType="number"
                android:textStyle="bold"
                android:hint="Room Id"
                android:gravity="center_horizontal"/>

            <Button
                android:id="@+id/button_join"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="32dp"
                android:paddingLeft="64dp"
                android:paddingRight="64dp"
                android:text="Join"
                android:textColor="#FFFFFF"
                android:backgroundTint="#388E3C" />

        </LinearLayout>


    </RelativeLayout>

</layout>

Other than the ViewModel binding here, all you have to know about this layout is the edit text field for the RoomId, and the Button to kick things off, verify the roomId, and start an intent to the MainActivity.

As for the Kotlin code, here are the important functions.

private fun observeRoom() {
    viewModel.getRoom().observe(this, Observer { room ->
        if (room != null) {
            handleRoomJoined(room.id)
        }
    })
}

private fun handleSendButton() {
    viewModel.joinRoom(input_roomid.text.toString())
}

private fun handleRoomJoined(roomId: String) {
    val preferences = getSharedPreferences("MAIN_PREFERENCES", Context.MODE_PRIVATE)
    preferences.edit()
        .putString("ROOM_ID", roomId)
        .apply()

   launchChatroom(roomId)
}

private fun launchChatroom(roomId: String) {
    val intent = Intent(this, MainActivity::class.java)
    intent.putExtra("ROOM_ID", roomId)
    startActivity(intent)
    finish()
}

When the Send Button is clicked, the task is delegated to the ViewModel to verify the room and either join or create it. Once either operation completes, observeRoom() will receive the room from the LiveData, save it to preferences for later use, then launch the MainActivity, otherwise known as the chatroom. That about wraps it up for the LobbyActivity.

I only included the key functions here related to our Chatkit data flow, but if you want the full code, I linked the Github repository for this project below.

MainActivity (Chatroom)

Again, let’s start with the ViewModel.

MainViewModel

class MainViewModel: BaseViewModel() {

    private lateinit var chatRepository: ChatRepository

    fun initChatbotRepository(context: Context, roomId: String) {
        chatRepository = ChatRepository(context, roomId)
    }

    fun getRoom(): LiveData<Room> {
        return chatRepository.room
    }

    fun getMessages(): LiveData<List<Message>> {
        return chatRepository.messages
    }

    fun sendMessage(message: String) {
        chatRepository.sendMessage(message)
    }

    fun leaveRoom() {
        chatRepository.leaveRoom()
    }

    override fun onCleared() {
        super.onCleared()
        if (this::chatRepository.isInitialized)
            chatRepository.terminate()
    }
}

 

The ViewModel here simply establishes the connection between the View and the Repository. Again, I would have ideally included more logic here by letting the ViewModel handle the results of a SynchronousChatManager.

MainActivity

Once again, some copy-pastable XML code.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".activities.main.MainActivity">

    <data>
        <variable
            name="mainViewModel"
            type="com.example.pusherbot.activities.main.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#FAF3F3"
        android:orientation="vertical" >

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/chat_messages_list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="#FFFFFF"
            android:orientation="horizontal"
            android:layout_gravity="bottom">

            <EditText
                android:id="@+id/chat_input"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:hint="Send message..."
                android:paddingStart="16dp"
                android:background="@android:color/transparent"
                android:layout_gravity="center_vertical"
                android:textSize="18sp" />

            <ImageButton
                android:id="@+id/chat_send_button"
                android:background="@android:color/transparent"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:scaleType="centerInside"
                android:src="@drawable/ic_send_black"
                android:tint="@color/colorPrimary"
                android:layout_marginEnd="16dp"
                android:padding="4dp"
                android:layout_gravity="center_vertical" />

        </LinearLayout>

    </LinearLayout>

</layout>

The two components you need to know about here are the RecyclerView of messages in the chat, and a layout of an EditText and Button for sending messages.

Speaking of the RecyclerView, here’s the adapter I’m using.

class ChatAdapter(private val messages: List<Message>): RecyclerView.Adapter<ChatAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item_chat, parent, false))
    }

    override fun getItemCount(): Int {
        return messages.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val message = this.messages[position]

        holder.sender.text = message.sender.name
        when(val data = message.parts[0].payload) {
            is Payload.Inline -> holder.message.text = data.content
        }
    }


    class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
        val sender = view.chat_sender
        val message = view.chat_message
    }

}

And the list item layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <TextView
        android:id="@+id/chat_sender"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textColor="#1A1A1A"
        android:textStyle="bold"
        tools:text="John" />

    <TextView
        android:id="@+id/chat_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:layout_marginTop="8dp"
        android:textColor="#1A1A1A"
        tools:text="Hello World" />

</LinearLayout>

Now finally, the Activity Kotlin code.

private fun initRoom() {
    val roomId = intent.getStringExtra("ROOM_ID")
    if (roomId == null) {
        handleNullRoomId()
        return
    }

    supportActionBar?.title = "Room $roomId"

    viewModel.initChatbotRepository(this, roomId)
}

private fun observeRoom() {
    viewModel.getRoom().observe(this, Observer { room ->
        if (room == null)
          handleLeaveRoom()
    })
}

private fun observeMessages() {
    viewModel.getMessages().observe(this, Observer { messages ->
        handleMessages(messages)
    })
}

private fun handleMessages(messages: List<Message>) {
    val chatAdapter = ChatAdapter(messages)
    chat_messages_list.layoutManager = LinearLayoutManager(this)
    chat_messages_list.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
    chat_messages_list.adapter = chatAdapter
    chat_messages_list.layoutManager?.scrollToPosition(messages.size - 1)
}

private fun handleSendButton() {
    sendMessage(chat_input.text.toString())
    chat_input.setText("")
}

private fun sendMessage(message: String) {
    viewModel.sendMessage(message)
}

private fun handleLeaveRoom() {
    val preferences = getSharedPreferences("MAIN_PREFERENCES", Context.MODE_PRIVATE)
    preferences.edit()
        .putString("ROOM_ID", null)
        .apply()

    startActivity(Intent(this, LobbyActivity::class.java))
    finish()
}

private fun handleNullRoomId() {
    startActivity(Intent(this, LobbyActivity::class.java))
    finish()
}

This is a bit more involved so I’ll explain what’s going on here.

initRoom() just starts the ChatbotRepository, and includes a safety check. We don’t want a null RoomId in this activity. If that situation arises, we can simply send the user back to the LobbyActivity.

We then have our LiveData observers. observeRoom() is used just for checking if the user left the room, like through the options menu. observeMessages() of course receives the messages of the chat, then passes them onto the adapter.

I think the rest is pretty self-explanatory.

Get the Sample on Github

All the code here can be found on Github. It’s a bit more involved as it uses a LoginActivity with Firebase Auth to get a unique userID for each user, but it’s not fully implemented so you can ignore most of that for now. I’ll get back to it once I manage to successfully make use of Chatkit’s API interface to create users. For now, we can hardcode John. I’m sure he’s fine with that.