Tutorial: Build an Android chat app with PubNub
Learn how to build a scalable chat app using the PubNub Android SDK getting started tutorial.
Build a Chat App with the PubNub Android SDK
This tutorial will walk you through developing a basic chat application in Kotlin using PubNub.
This tutorial will focus on the Kotlin SDK and not the community-supported chat UI components designed for Kotlin.
The source code for this application can be found on Github at https://github.com/PubNubDevelopers/GettingStarted-Kotlin-SDK-Tutorial
Prerequisites
Before running this application, be sure you have the following installed:
Android Studio. The application was created with version ‘chipmunk’ but should work with any recent version of the IDE.
Download Source Application
In a terminal, navigate to an empty folder. Enter the following command to clone the repository:
git clone https://github.com/PubNubDevelopers/GettingStarted-Kotlin-SDK-Tutorial
cd GettingStarted-Kotlin-SDK-Tutorial
The main git branch contains the completed version of this tutorial. If you would like to follow along, please checkout the following branch:
git checkout tutorial_step_0
Create keys
Demo keys
You're ready to go!
We’ve assigned demo keys which are set up to support the features of this tutorial app. These will be active for 72 hours but you can sign up for a free account at the end of the tutorial to create your own permanent keys.
The demo keys have the following functionality activated ready for your application:
Presence
Message Persistence You will be able to see user’s message history and meta data.
Stream Controller You will be able to receive messages against a group of channels, or post one message to multiple channels.
App Context This allows you to manage the metadata of your users and channels.
My own keys
You'll need to create an account to use your own keysSign in to your PubNub Dashboard.
Go to Keysets using the left hand side navigation.
Click the “Create New Keyset” button on the upper right hand-side of the portal.
A pop-up appears. Give the keyset a name and click the “Create” button.
Click on the newly created keyset.
Enable four features for the keyset to be able to connect to the PubNub Network: Presence, Message Persistence, Stream Controller and App Context.
Enable Presence by clicking on the toggle to turn it on. A pop-up will require that you enter in “ENABLE”. Enter in “ENABLE” in all caps and then press the “Enable” button. You can leave the default settings.
Enable Message Persistence by clicking on the toggle to turn it on. You can leave the default settings.
Enable Stream Controller by clicking on the toggle to turn it on if it is already not enabled. You can leave the default settings.
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.
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.
Scroll back to the top of the portal. Copy the Publish key by clicking on the copy icon and paste it below
Copy the Subscribe Key by clicking on the copy icon and paste it below
Build and run
Watch the video or follow the steps below to build and run your application.
//This code box will update soon as you
//progress through the video
//Use the copy icon in the top right to quickly copy
//code as you're following alongStep 1. Add PubNub Keys
Open the gradle.properties file and enter the publish and subscribe keys at the relevant location, found towards the bottom of the file
#PubNub Keys
PUBNUB_PUBLISH_KEY = "REPLACE WITH YOUR PUBNUB PUBLISH KEY"
PUBNUB_SUBSCRIBE_KEY = "REPLACE WITH YOUR PUBNUB SUBSCRIBE KEY"You will need to perform a Gradle sync to have these settings apply.
Step 2. Update the Android Manifest
Add two uses-permission tags to your Android manifest, just above the <application/> tag:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />These are both required for PubNub to work on your Android device or emulator
Step 3. Adding chat to your application with PubNub
The application on the main branch is fully functional and contains all the changes described in the following steps.
If you want to follow along with the tutorial and build the functionality into a templated version of the app, please checkout the following branch:
git checkout tutorial_step_0Step 3a. Creating the PubNub object
Before interacting with PubNub, you first need to create a PubNub configuration instance. This requires 3 things, the publish and subscribe keys that were created in the previous step and a device identifier.
The device identifier you pass in should represent a unique instance of the user or device that connects to PubNub. If you maintain your own user registration workflow then you could use whichever unique identifier you assigned the user during registration. If your users do not already have an assigned ID, as in the case of this app, you should assign a device-specific identifier.
Android exposes several IDs that can be used for this purpose such as the Firebase installation ID, a privately generated GUID, or a user-resettable identifier such as Android_ID
deviceId = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)
val config = PNConfiguration(UserId(deviceId)).apply {
publishKey = BuildConfig.PUBLISH_KEY
subscribeKey = BuildConfig.SUBSCRIBE_KEY
}
pubnub = PubNub(config)Step 3b. Subscribing to a channel when in the foreground
The concepts of publish and subscribe are fundamental to PubNub where messages are sent from a publishing source and received by subscribers over a channel. You do not need to create the channel separately, it will be created automatically for you by PubNub whenever the first user publishes or subscribes to it.
Despite the apparent simplicity, the Pub/Sub model is able to accommodate the most complex of communication use cases.
To keep this application simple we will only have a single channel to represent the application’s single "chat room". The application will subscribe to receive messages from the channel that have been published by other users.
Since you cannot rely on your application being active in the background on Android most chat developers will take a two stage approach:
Subscribe to receive messages when the app comes to the foreground
Unsubscribe when the app goes into the background
Receive a mobile push notification whilst in the background to notify the user that a message is available. This is outside of the scope of this tutorial but please see our Push Notifications tutorial for more information.
Since we want to receive presence events when users join or leave the channel, specify ‘withPresence’ to be true.
Add this code to the onResume() function:
pubnub.subscribe(
channels = listOf(groupChatChannel),
withPresence = true
)Add this code to the onPause() function:
pubnub.unsubscribe(
channels = listOf(groupChatChannel)
)Step 3c. Publishing messages to a channel
Messages published to a channel will be received by all subscribers on that channel.
The message payload is a String but you can also optionally add custom ‘meta’ information to the message as a JSON object. This application only uses a data payload.
pubnub.publish(
channel = groupChatChannel,
message = message
).async { result, status ->
if (!status.error) {
// Handle any errors
}
}Step 3d. Setting up a PubNub listener
PubNub event listeners are used to receive:
Status information: A Pub/Sub call has completed
Presence information: A user has joined or left a channel.
Messages: A message has been received.
Signals: A signal has been received.
Message Reactions: Used to modify messages after they are published, for example adding an emoji reaction.
App Context: PubNub's serverless storage has been updated
Many of these listeners require the 'App Context' feature to be enabled on the keyset, which was done during the ‘Create Keys’ section of this tutorial.
This tutorial only uses a subset of the possible listeners which will be populated in upcoming steps.
val listener = object : SubscribeCallback() {
override fun status(pubnub: PubNub, pnStatus: PNStatus) {
// Not used in this application
}
override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {
// todo
}
override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {
// todo
}
override fun objects(pubnub: PubNub, objectEvent: PNObjectEventResult) {
// todo
}
}Having created the listener object it must be added to the PubNub object, created in step 3a. Add the following code after the listener declaration.
pubnub.addListener(listener)Step 3e. Listening for new messages
Once an application subscribes to messages on a channel, the messages will be received by the message listener, created at step 3d.
Populate the message override as follows:
override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {
if (pnMessageResult.channel == groupChatChannel) {
// Display the message by adding it to the viewModel
val newMsg = Message()
newMsg.message = pnMessageResult.message.asString
newMsg.senderDeviceId = pnMessageResult.publisher.toString()
newMsg.timestamp = pnMessageResult.timetoken!!
chatViewModel.messages.add(newMsg)
}
}Step 3f. Listening for presence events
By subscribing to a channel’s presence events, as was done in step 3b, we will also receive events whenever a user joins or leaves the channel.
Modify the presence listener, created in step 3d, as follows to update the list of users present in the chat.
override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {
when (pnPresenceEventResult.event) {
"join" -> {
addMember(pnPresenceEventResult.uuid.toString())
}
"leave" -> {
removeMember(pnPresenceEventResult.uuid.toString())
}
"interval" -> {
// 'join' and 'leave' will work up to the ANNOUNCE_MAX setting (defaults to 20 users)
// Over ANNOUNCE_MAX, an 'interval' message is sent.
// The below logic requires that 'Presence Deltas' be defined for the keyset, explained later
pnPresenceEventResult.join?.forEach { userId ->
addMember(userId)
}
pnPresenceEventResult.leave?.forEach { userId ->
removeMember(userId)
}
}
}
}When the application first starts, it needs to populate the list of users currently in the channel. Since presence events only apply to users newly joining or leaving, the application calls the ‘hereNow’ function on launch to retrieve a list of device IDs already in the group chat.
pubnub.hereNow(
channels = listOf(groupChatChannel),
includeUUIDs = true
).async {
result, status ->
if (!status.error) {
// The API will return an array of occupants in the channel, defined by their
// ID. This application will need to look up the friendly named defined for
// each of these ID (later)
result?.channels?.get(groupChatChannel)?.occupants?.forEach { i ->
addMember(i.uuid)
}
} else {
// Handle error
}
}Note that addMember() and removeMember() are responsible for updating the chat viewmodel and resolving UUIDs to friendly names.
Step 3g. Retrieving past messages
When a user first joins the chat, it is often useful to provide some recently sent messages, to give some context of any conversation in progress.
Alternatively, if a user leaves and then rejoins the chat they will need to get up to speed on any messages they missed whilst they were way.
Both of these use cases are handled by the PubNub Message Persistence API.
In the onCreate function, invoke the history method to receive the past few messages
pubnub.fetchMessages(
channels = listOf(groupChatChannel),
page = PNBoundedPage(limit = 8),
includeUUID = true
).async { result, status ->
if (!status.error) {
result!!.channels.forEach { (channel, messages) ->
if (channel == groupChatChannel) {
messages.forEach { messageItem: PNFetchMessageItem ->
val newMsg = Message()
newMsg.message = messageItem.message.asString
newMsg.timestamp = messageItem.timetoken
newMsg.senderDeviceId = messageItem.uuid.toString()
chatViewModel.messages.add(newMsg)
lookupMemberName(newMsg.senderDeviceId)
}
}
}
} else {
// handle the error
}
}Step 3h. Assign a friendly name to the device and persist it with PubNub object storage
App Context is a very powerful serverless storage mechanism provided by PubNub and designed to meet many use cases of a standard chat app.
For example, you can choose to use App Context to store:
User meta data such as name, avatar, email
Channel metadata such as name and description as well as who the members of that channel are
This application does not take full advantage of the App Context but does store a single piece of information, the ‘friendly name’ that the user can assign themselves. Assigning a friendly name avoids having to display the long user id in the member list.
Setting the ‘friendly name’ is handled in a separate activity, modify the ‘save’ button onClick() handler in SettingsUI.kt to write the name to the PubNub user meta data.
pubnub.setUUIDMetadata(
name = text.text
).async { result, status ->
if (status.error) {
// Handle error
}
}Step 3i. Lookup someone else's friendly name using PubNub Object storage
The PubNub App Context is considered the master copy of ‘friendly names’ set by anyone involved in the chat, as detailed in step 3h.
The presence listener, described in step 3f, only tells you the user ids of users joining or leaving the chat. To update the list of member friendly names in the chat, add the following code to the lookupMemberName() function, in MainActivity
pubnub.getUUIDMetadata(
uuid = deviceId,
).async { result, status ->
if (!status.error) {
// Add the user's name to the memberNames hashmap (part of the viewModel, so
// the UI will update accordingly)
chatViewModel.memberNames.put(deviceId, result?.data?.name.toString())
} else {
// Handle error
}
}What about if another user changes their friendly name? To be notified when the friendly name changes, add an "objects" override to the PubNub listener you created in step 3d, this objects listener will fire whenever any user meta data is updated.
override fun objects(pubnub: PubNub, objectEvent: PNObjectEventResult) {
if (objectEvent.extractedMessage.type.equals("uuid")) {
val extractedMessage: PNSetUUIDMetadataEventMessage =
objectEvent.extractedMessage as PNSetUUIDMetadataEventMessage
replaceMemberName(
extractedMessage.data.id,
extractedMessage.data.name.toString()
)
}
}One additional point about the ‘objects’ listener: You must have “User Metadata Events” applied to your keyset, as below.
Notes and other caveats
For efficiency, PubNub handles presence slightly differently depending on how many occupants you have in the channel. If using this app with a number of users above the ‘Announce Max’ setting, which defaults to 20, be sure to enable the ‘Presence Deltas’ option in your admin dashboard. This causes PubNub to report the user IDs who have joined or left as part of the interval event, which this app uses to maintain who is in the channel.
Congratulations - what's next?
Chat SDK Documentation
Chat OverviewKotlin SDK DocsChat Developer PathLive Tour
Start the live coding tourDeveloping to a tight deadline? Explore Chat Components
Chat Components IntroductionChat Components Documentation (Android)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 ExpertTake it to production on your own keys
Create a free PubNub account to get permanent keys for this app.