Using the PubNub Kotlin Chat SDK

PubNub's Kotlin Chat SDK is the recommended way for Android developers to create new chat applications or add in-app chat to existing applications

Step 01

Chat SDK Tutorial: Environment Setup for Kotlin

This tutorial walks you through a basic Android sample app created with PubNub Kotlin Chat SDK 1.0.0. It explains how the sample is structured, how the app initializes chat, and how the messaging features map to the Kotlin Chat SDK APIs. The sample app is available at https://github.com/PubNubDevelopers/Chat-SDK-Tutorial-Kotlin and is released under MIT. The sample is designed for learning and has minimal error handling.


Note: This written tutorial and the repository have been refreshed for PubNub Kotlin Chat SDK 1.0.0. Some video footage and screenshots may show an earlier version of the UI, but the code snippets below match the upgraded sample.


Prerequisites

The Kotlin Chat SDK is UI and framework-agnostic, so you control how your application looks and how it is architected. The sample application uses Jetpack Compose with a small custom UI.


Make sure you have the following installed on your computer:

Download the Source Application

In a terminal or command window, navigate to an empty folder. Enter the following command to clone the repository.

Code
git clone https://github.com/PubNubDevelopers/Chat-SDK-Tutorial-Kotlin.git

Install dependencies

Open the Chat-SDK-Tutorial-Kotlin folder in Android Studio. Gradle will download the required Android, Compose, and PubNub dependencies. If you experience build issues with a newer Android Studio version, contact the DevRel team at devrel@pubnub.com

Open the following file:

/app/src/main/java/com/pubnub/chatsdk/kotlintutorial/Keys.kt

The file contains placeholders for your PubNub publish and subscribe keys:

Code
package com.pubnub.chatsdk.kotlintutorial

//  REPLACE THESE WITH YOUR PUB / SUB KEYS
val PUBNUB_PUBLISH_KEY="PUBLISH_KEY"
val PUBNUB_SUBSCRIBE_KEY="SUBSCRIBE_KEY"

The next step is to create your own publish and subscribe keys to replace these placeholders.

Step 02

Create keys

Demo keys

You're ready to go!

We have assigned demo keys that are set up to support the features of this tutorial app. These keys will be active for 72 hours, but you can sign up for a free account at the end of the tutorial to create permanent keys for your own app.

The demo keys have the following functionality activated:

  • Presence This lets the app show whether users are online and powers presence-aware UI.
  • Message Persistence This stores message history so the app can load earlier messages and reactions when a user reopens a channel.
  • App Context This stores user, channel, and membership metadata used by the sample app.

My own keys

  1. You'll need to create an account to use your own keysSign in to your PubNub Dashboard.
  2. Go to Keysets using the left hand side navigation.
  3. Click the “Create New Keyset” button on the upper right hand-side of the portal.
  4. A pop-up appears. Give the keyset a name and click the “Create” button.
  5. Click on the newly created keyset.
  6. Enable three features for the keyset: Presence, Message Persistence, and App Context.
  7. Enable Presence by clicking on the toggle to turn it on. A pop-up will require that you enter "ENABLE". Enter "ENABLE" in all caps and press the "Enable" button. You can leave the default settings.
  8. Enable Message Persistence by clicking on the toggle to turn it on. You can leave the default settings.
  9. Enable App Context by clicking on the toggle to turn it on if it is not already enabled. Select the region closest to you to store your context.
  10. Click on the “Save Changes” button in the bottom right corner of the portal. A confirmation message will appear in the upper-left corner of the portal confirming that these changes have been saved. 
  11. Scroll back to the top of the portal. Copy the Publish key by clicking on the copy icon and paste it into your app.
  12. Copy the Subscribe key by clicking on the copy icon and paste it into your app.


Step 03

Build and run

Watch the video or follow the steps below to build and run your application.

Add PubNub Keys

In the previous parts of this tutorial, you cloned the sample application, loaded it into Android Studio, installed dependencies, and created PubNub keys. Locate the file /app/src/main/java/com/pubnub/chatsdk/kotlintutorial/Keys.kt and replace the placeholder publish and subscribe keys with the keys you generated in the previous step.

Kotlin
package com.pubnub.chatsdk.kotlintutorial

//  REPLACE THESE WITH YOUR PUB / SUB KEYS
val PUBNUB_PUBLISH_KEY="PUBLISH_KEY"
val PUBNUB_SUBSCRIBE_KEY="SUBSCRIBE_KEY"

Run the Application

Everything is now configured to run the application. Launch two emulators, two devices, or one emulator and one device, then open the application on both.

Tutorial - Kotlin Chat SDK - Login

Enter a user name and log in. The screenshot above uses Esther and Brooklyn.

The first time you run the application it may need to create user and channel metadata. If the first view does not show the expected public channels or direct chat options, log out and log back in on each device.

You should see two public channels and the option to launch a direct chat with the other device.

Tutorial - Kotlin Chat SDK - Home Screen

Try the following:

  • Open a public channel on both devices and send a message. The avatar for each user is randomly selected when the user is first created.
  • React to a message by selecting one of the two emoji. The reaction update is sent to all active recipients.
  • Back out of the public channel and reopen it. Message history and reactions are retained.
  • Open a direct chat between the two participants.
  • Send a direct message from one user to the other. The sample shows typing status and a read receipt after the message has been read.

How the Sample App Works

The rest of this tutorial reviews the main features of the sample application and explains the Kotlin Chat SDK APIs used by the app. The sample uses Jetpack Compose, but the PubNub Kotlin Chat SDK itself is UI-agnostic.

Initializing the Chat SDK

The first step is to create a Chat object and initialize it.

Kotlin
fun createChat(chatConfiguration: ChatConfiguration, userId: String): PNFuture<Chat> {
    return Chat.init(
        chatConfiguration,
        PNConfiguration.builder(
            UserId(userId),
            PUBNUB_SUBSCRIBE_KEY
        ) {
            publishKey = PUBNUB_PUBLISH_KEY
        }.build()
    )
}

The publish and subscribe keys identify your PubNub keyset. The userId uniquely identifies the human user to PubNub. In production, this value usually comes from your authentication or identity system.

The sample app converts the name typed on the login screen into a simple PubNub user ID by replacing unsupported characters:

Kotlin
private fun convertToId(name: String): String {
    val pattern = "[^a-zA-Z0-9]".toRegex()
    return name.replace(pattern, "-").lowercase()
}

The home screen initializes chat with a five second typing timeout:

Kotlin
chat = createChat(
    ChatConfiguration(LogLevel.VERBOSE, typingTimeout = 5.seconds),
    userId
).await()

After the Chat SDK is initialized for a user ID, the app updates the current user's profile metadata if needed:

Kotlin
if (chat?.currentUser?.profileUrl == null || chat?.currentUser?.profileUrl == "") {
    chat?.currentUser?.update(
        name = name,
        profileUrl = randomProfileUrl
    )?.await()
}

Creating Public Channels

Conversations using the Chat SDK happen over a Channel. The sample uses two channel types:

  • Public: a channel anybody can participate in. Public chats do not show read receipts or typing indicators in this sample.
  • Direct: a private conversation between two participants.

When the sample app is first initialized, it checks whether the public channels already exist:

Kotlin
publicChannels =
    chat?.getChannels(filter = "id LIKE \"public*\"")?.await()?.channels?.toList()

If the channels do not exist, the app creates them with chat.createPublicConversation():

Kotlin
val customObjectGeneral = JSONObject()
customObjectGeneral.put(
    "profileUrl",
    "$avatarBaseUrl/group/globe1.png"
)
chat?.createPublicConversation(
    channelId = "public-general",
    channelName = "General Chat",
    channelDescription = "Public group for general conversation",
    channelCustom = customObjectGeneral
)?.await()

In production, public channels are usually created on the server or by an administrator. The tutorial keeps channel creation in the client so the sample can run without extra infrastructure.

If you have access to BizOps Workspace, you can see the created public channels in Channel Management.

Tutorial - Kotlin Chat SDK - Public Channels

Displaying and Creating 1:1 Conversations

During initialization, the sample queries the ten most recently updated users and shows them in the direct messages list:

Kotlin
allUsers = chat?.getUsers(
    sort = listOf(PNSortKey.desc(PNKey.UPDATED)),
    limit = 10
)?.await()?.users?.toList()

This keeps the sample simple. A production app would usually provide search, invitations, contact lists, or some other controlled way to choose conversation participants.

When a user is selected, the app creates or retrieves a direct conversation with chat.createDirectConversation():

Kotlin
val result = chat.createDirectConversation(selectedUser).await()
activeChannel = result.channel

The returned result contains the direct channel. If the conversation already exists, the SDK returns the existing channel.

You can see the created direct channel in BizOps Workspace if BizOps is enabled on your account.

Tutorial - Kotlin Chat SDK - Direct Channels

Sending and Receiving Messages

Sending messages is handled at the channel level with channel.sendText():

Kotlin
activeChannel?.sendText(messageText)?.await()

The app joins the selected channel so the current user has a membership for unread counts and read receipts:

Kotlin
val myChannelMembership: Membership? =
    channelMemberships?.firstOrNull() ?: activeChannel?.join()?.await()

Message receipt is handled by channel.onMessageReceived():

Kotlin
channelMessageSubscription = activeChannel?.onMessageReceived {
    messages = messages + it
    scope.launch {
        myChannelMembership?.setLastReadMessage(it)?.await()
    }
    restartMessageUpdates()
}

The returned AutoCloseable is closed when the active channel changes so the app stops receiving messages for the previous channel.

Message History

To retrieve messages sent before the user opened the channel, enable Message Persistence on your PubNub keyset and call channel.getHistory():

Kotlin
activeChannel?.getHistory(count = 20)?.await()?.let {
    messages = it.messages
    messages.lastOrNull()?.let { lastMessage ->
        myChannelMembership?.setLastReadMessageTimetoken(lastMessage.timetoken)?.await()
    }
    restartMessageUpdates()
}

The sample loads the most recent 20 messages. The API can return up to 25 messages in a single call. To retrieve more history, call getHistory() multiple times and update startTimetoken and endTimetoken.

Message Reactions

The Chat SDK lets users react to messages and receive updates when reactions change.

Tutorial - Kotlin Chat SDK - Reactions

To apply or remove an emoji reaction, call message.toggleReaction():

Kotlin
message?.toggleReaction(emoji)?.await()

There is one toggle API rather than separate add and remove APIs. Emojis are represented as Unicode strings, so the API can work with new emoji introduced in the future.

The sample uses two hardcoded emoji that are shown on every message. A production app would usually let users choose from a reaction picker or a long-press menu.

To receive updates when a reaction changes, use Message.streamUpdatesOn():

Kotlin
fun restartMessageUpdates() {
    messageActionsStream?.close()
    messageActionsStream = if (messages.isNotEmpty()) {
        Message.streamUpdatesOn(messages = messages) { updatedMessages ->
            messages = updatedMessages.toList()
        }
    } else {
        null
    }
}

Whenever a message changes, the callback receives the updated message collection. The sample replaces the message list to keep the state management simple.

Displaying message reactions

Reactions applied to a message are available through message.reactions:

Kotlin
message?.reactions

Each reaction includes the reaction value, whether it belongs to the current user, the user IDs that added it, and the total count:

Kotlin
MessageReaction(
    value = "😀",
    isMine = true,
    userIds = listOf("esther", "brooklyn")
)

The sample determines the visible count for each displayed emoji like this:

Kotlin
val count = message?.reactions?.firstOrNull { it.value == emoji }?.count ?: 0

The platform is responsible for displaying emoji. Only exchange emoji that can be displayed on the recipient platform, or use an emoji compatibility library.

You can also check whether the current user has reacted with a specific emoji by calling message.hasUserReaction(emoji).

Read Receipts

Read receipts are available for group and direct channels. They are not shown for public channels in this sample.

Tutorial - Kotlin Chat SDK - Read Receipts

At a high level, read receipts work like this:

  • A client receives a message.
  • The receiving client marks that message as read.
  • Other clients in the chat are notified that the user's last-read timetoken changed.

Deciding when a message is read is an application-level choice. Many chat apps only mark a message as read after it appears in the visible scroll area. This sample keeps the logic simple and marks messages as read when they are received on the active channel.

The Chat SDK stores read state on the Membership, because read state belongs to the relationship between a user and a channel:

Kotlin
myChannelMembership?.setLastReadMessage(it)?.await()

When history is loaded, the app marks the newest historical message as read:

Kotlin
messages.lastOrNull()?.let { lastMessage ->
    myChannelMembership?.setLastReadMessageTimetoken(lastMessage.timetoken)?.await()
}

Fetching and receiving read receipts

The app fetches the current read receipt state when a direct channel opens, then subscribes to new read receipt events:

Kotlin
if (activeChannel?.type != ChannelType.PUBLIC) {
    readReceipts = activeChannel?.fetchReadReceipts()?.await()?.receipts ?: emptyList()
    readReceiptStream = activeChannel?.onReadReceiptReceived { receipt ->
        updateReadReceipt(receipt)
    }
}

Each receipt contains a userId and the user's latest lastReadTimetoken:

Kotlin
fun updateReadReceipt(receipt: ReadReceipt) {
    readReceipts = readReceipts.filterNot { it.userId == receipt.userId } + receipt
}

The sample uses the following basic logic to decide whether to show a read checkmark for a sent message:

Kotlin
val isRead = activeChannel?.type != ChannelType.PUBLIC &&
    message != null &&
    readReceipts.any {
        it.userId != chat?.currentUser?.id && it.lastReadTimetoken >= message.timetoken
    }

This binary display is enough for a direct chat. In group conversations, you would usually show a more detailed state such as "Read by Esther and Brooklyn" by resolving receipt user IDs with chat.getUser(userId).

Typing Indicator

Typing indicators are available for direct and group chats.

Tutorial - Kotlin Chat SDK - Typing Indicator

The SDK handles the timeout logic usually associated with implementing typing indicators. Your app tells the SDK when somebody is typing, and the SDK emits updates when the active typing user list changes.

In the message input change handler, the sample calls channel.startTyping():

Kotlin
if (channel.type != ChannelType.PUBLIC) {
    channel.startTyping().await()
}

The Chat SDK treats repeated calls as "the user is still typing" and uses typingTimeout to decide when the user stopped typing. This sample configures typingTimeout = 5.seconds during chat initialization.

You can also manually call channel.stopTyping(), but most apps can rely on the timeout mechanism.

The sample uses a separate TypingIndicator component and subscribes to typing changes with channel.onTypingChanged():

Kotlin
if (channel.type != ChannelType.PUBLIC) {
    val typingIndicators: Collection<String> by produceState<Collection<String>>(listOf()) {
        val sub = channel.onTypingChanged {
            value = it
        }
        awaitDispose {
            sub.close()
        }
    }

    if (typingIndicators.isNotEmpty()) {
        // Update display of active typers.
    }
}

The API returns user IDs. A production app will often convert those IDs to names and avatars using chat.getUser(userId).

You have now finished the tutorial. Click "Next" below for the next steps.

Step 04

Congratulations - what's next?

Chat SDK Documentation

Understand the Source Code

This tutorial has only scratched the surface of the Kotlin Chat SDK. Check out the SDK source code, available at https://github.com/pubnub/kmp-chat

The source code for the sample app discussed in this tutorial is available from https://github.com/PubNubDevelopers/Chat-SDK-Tutorial-Kotlin

Chat SDK Demos in other Languages

Experience our fully-featured group chat app for the web, written in TypeScript and Next.js:

Tutorial - Chat SDK - React Chat Demo

Ready to get beyond Proof of Concept?

Schedule a 15 minute chat with one of our experts to get your project moving

Talk to an Expert

Got a question for our developer relations team? Email devrel@pubnub.com

Keep building

Take it to production on your own keys

Create a free PubNub account to get permanent keys for this app.