Chat SDK v4 2x

Swift, Kotlin, and TypeScript SDKs

Build in-app chat, calls, and live streaming

On This Page

How to build in-app chat using Kotlin – Part 2

Alex Preston, Solutions Engineer
Alex Preston
Solutions Engineer
  • Tutorial Type: Basic
  • Reading Time: 30 mins
  • Building Time: 2 hrs
Chat SDK v4 2x

Swift, Kotlin, and TypeScript SDKs

Build in-app chat, calls, and live streaming

Chat SDK v4 2x

Swift, Kotlin, and TypeScript SDKs

Build in-app chat, calls, and live streaming

On This Page

Introduction

This tutorial follows part 1 of the tutorial on how to build in-app chat using Kotlin. This 2-part guide aims to help you get up and running by showcasing a simple chat implementation.

Getting started with the UIKit is wonderfully accessible, but in some cases, you may need to implement chat from our Core SDK. This guide picks up where we left off in part 1. In part 1, we built out the:

  • login
  • ability to create and list channels

In this tutorial, we will create the:

  • UI for the ChatActivity
  • MessageAdapter which sets the UI components
  • ChannelActivity class

Before we dive in, please note the following:

1. a. The diagram below depicts class relationships. You may find it to be a helpful reference as you proceed through this tutorial.

Diagram 1: Class diagram for the Kotlin sample
Diagram 1: Class diagram for the Kotlin sample

b. This guide only covers sending and receiving User Messages. To see how to send File Messages, please visit our docs.

c. This guide focuses on three different parts:

  • UI for the Channel Activity
  • MessageAdapter.kt class
  • ChannelActivity.kt class

d. This tutorial was built using:

  • Android Studio: 4.0.2
  • Android Version: 10 API 29
  • Kotlin: 1.3.72
  • Sendbird Core SDK: 3.0.148

e. This tutorial assumes prior knowledge of Android and Android concepts.

f. Here is the completed source code for both part-1 and part-2.

Step 1: Building the UI for the channel activity

Step 1: Building the UI for the channel activity

1. activity_chat.xml
The first thing we will need to do is create a UI for how the chat will look. We will add an AppBarLayout with a “Back” button on the top to return to the ChannelListActivity. Below that, we’ll add a RecyclerView to show the actual messages. Finally, at the bottom, we’ll add a simple layout to handle entering and sending messages. This is the gist:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/layout_group_chat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_gchannel"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/Widget.AppCompat.Toolbar"
app:popupTheme="@style/Theme.AppCompat.Light">
<TextView
android:id="@+id/button_gchat_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Back"
android:layout_marginTop="16dp"
android:textColor="#e0e0e0"
android:textSize="20sp"
/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_gchat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/text_gchat_indicator"
app:layout_constraintTop_toBottomOf="@+id/layout_group_chat" />
<TextView
android:id="@+id/text_gchat_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/view"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e0e0e0"
app:layout_constraintBottom_toTopOf="@+id/layout_gchat_chatbox" />
<RelativeLayout
android:id="@+id/layout_gchat_chatbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:layout_marginStart="16dp"
android:id="@+id/edit_gchat_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/button_gchat_send"
android:background="@android:color/transparent"
android:hint="Enter Message"
android:inputType="text"
android:maxLines="6"
tools:ignore="Autofill" />
<Button
android:id="@+id/button_gchat_send"
android:layout_width="64dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:background="?attr/selectableItemBackground"
android:text="Send"
android:textColor="@color/colorPrimary" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

2. item_chat_me.xml
Now that we have completed the UI for the skeleton of this Activity, we will need to create two different item views for the messages. We will need to do this because we will have a different UI for messages sent by the current user, and we will have a different UI for messages sent by others in the chat. (As you implement more types of messages, you’ll have more such .xmls.)

The first .xml will be for messages that are from the “Me” perspective, or messages sent by the current users. For this, we have opted to have a TextView for the actual message, which is wrapped in a Cardview. Surrounding this TextView are other TextViews for things like the date. This is the gist:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/text_gchat_date_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:text="June 10"
android:textColor="#C0C0C0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/card_gchat_message_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="#774df2"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_gchat_date_me">
<LinearLayout
android:id="@+id/layout_gchat_container_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_gchat_message_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxWidth="260dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:text="This is a Message"
android:textColor="#ffffff"
android:textSize="16sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/text_gchat_timestamp_me"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10:00"
android:textColor="#C0C0C0"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="@+id/card_gchat_message_me"
app:layout_constraintEnd_toStartOf="@+id/card_gchat_message_me" />
</androidx.constraintlayout.widget.ConstraintLayout>

3. item_chat_other.xml
The second .xml will be for messages that are from the “Other” perspective or any message not from the current user. This UI will be similar to the “Me” UI; however, it is left aligned and contains information about the “Other” user. This includes things like an ImageView for the profile image, and a TextField for the user’s name. Other than that, the views are relatively similar. This is the gist:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/text_gchat_date_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:text="June 10"
android:textColor="#C0C0C0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image_gchat_profile_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:contentDescription="User Icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_gchat_date_other" />
<TextView
android:id="@+id/text_gchat_user_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="John Grady Cole"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@+id/image_gchat_profile_other"
app:layout_constraintTop_toBottomOf="@+id/text_gchat_date_other" />
<androidx.cardview.widget.CardView
android:id="@+id/card_gchat_message_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="#eef1f6"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:layout_constraintStart_toEndOf="@+id/image_gchat_profile_other"
app:layout_constraintTop_toBottomOf="@+id/text_gchat_user_other">
<LinearLayout
android:id="@+id/layout_gchat_container_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_gchat_message_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxWidth="260dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:text="Message"
android:textColor="#000000"
android:textSize="16sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/text_gchat_timestamp_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="8:00"
android:textColor="#C0C0C0"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="@+id/card_gchat_message_other"
app:layout_constraintStart_toEndOf="@+id/card_gchat_message_other" />
</androidx.constraintlayout.widget.ConstraintLayout>

Step 2. The MessageAdapter.kt class

Now that we have implemented the UI, we are going to implement the MessageAdapter.kt class. We are skipping the ChannelActivity.kt, as it makes more sense to talk about how the UI will be set before we get into things like channel handlers. This class will handle attaching data passed to it to a particular view in the recyclerView.

Now, create a class called MessageAdapter.kt. This class will extend RecyclerView.Adapter<RecyclerView.ViewHolder>(), so you will need to make sure to implement the following methods:

  1. onCreateViewHolder
    This will return the customViewHolder that corresponds to the type of message.
  2. getItemViewType
    This is where we figure out the message type. Currently, we only implement UserMessage. This function will determine whether it is a “Me” message or an “Other” Message. We will return accordingly.
  3. onBindViewHolder
    This function binds the messages to the views.
  4. getItemCount
    This function returns the current position of the message.

There are two additional functions we need to add. These are:

  1. loadMessages
    The point of this method is to load the initial past messages which we will get in the ChannelActivity.
  2. addFirst
    This function will just add recently sent or received messages to the adapter. Obviously, both of these functions will need to call notifyDataSetChanged() to update the recyclerView. This is the gist:

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.example.sendbirdkotlinblog.R
import com.sendbird.android.BaseMessage
import com.sendbird.android.SendBird
import com.sendbird.android.UserMessage
import kotlinx.android.synthetic.main.item_chat_me.view.*
import kotlinx.android.synthetic.main.item_chat_other.view.*
import java.text.SimpleDateFormat
import java.util.*
class MessageAdapter(context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_USER_MESSAGE_ME = 10
private val VIEW_TYPE_USER_MESSAGE_OTHER = 11
private var messages: MutableList<BaseMessage>
private var context: Context
init {
messages = ArrayList()
this.context = context
}
fun loadMessages(messages: MutableList<BaseMessage>) {
this.messages = messages
notifyDataSetChanged()
}
fun addFirst(message: BaseMessage) {
messages.add(0, message)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when(viewType) {
VIEW_TYPE_USER_MESSAGE_ME -> {
MyUserHolder(layoutInflater.inflate(R.layout.item_chat_me, parent, false))
}
VIEW_TYPE_USER_MESSAGE_OTHER -> {
OtherUserHolder(layoutInflater.inflate(R.layout.item_chat_other, parent, false))
}
else -> MyUserHolder(layoutInflater.inflate(R.layout.item_chat_me, parent, false)) //Generic return
}
}
override fun getItemViewType(position: Int): Int {
val message = messages.get(position)
when (message) {
is UserMessage -> {
if (message.sender.userId.equals(SendBird.getCurrentUser().userId)) return VIEW_TYPE_USER_MESSAGE_ME
else return VIEW_TYPE_USER_MESSAGE_OTHER
}
//Handle other types of messages FILE/ADMIN ETC
else -> return -1
}
}
override fun getItemCount() = messages.size
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
VIEW_TYPE_USER_MESSAGE_ME -> {
holder as MyUserHolder
holder.bindView(context, messages.get(position) as UserMessage)
}
VIEW_TYPE_USER_MESSAGE_OTHER -> {
holder as OtherUserHolder
holder.bindView(context, messages.get(position) as UserMessage)
}
//Handle other types of messages FILE/ADMIN ETC
}
}
...
}

After we have handled the essential functions for a recyclerView Adapter, we will need to implement our own customViewHolders. We have two inner classes denoted by:

1. MyUserHolder
This class simply binds the respective message sent by “Me” to the item, for which we have earlier created the view. This is the gist:

class MyUserHolder(view: View) : RecyclerView.ViewHolder(view) {
val messageText = view.text_gchat_message_me
val date = view.text_gchat_date_me
val messageDate = view.text_gchat_timestamp_me
fun bindView(context: Context, message: UserMessage) {
messageText.setText(message.message)
messageDate.text = DateUtil.formatTime(message.createdAt)
date.visibility = View.VISIBLE
date.text = DateUtil.formatDate(message.createdAt)
}
}
view raw MyUserHolder.kt hosted with ❤ by GitHub

2. OtherUserHolder
This class simply binds the respective message sent by “Other” to the item, for which we have earlier created the view.
This is the gist:

class OtherUserHolder(view: View) : RecyclerView.ViewHolder(view) {
val messageText = view.text_gchat_message_other
val date = view.text_gchat_date_other
val timestamp = view.text_gchat_timestamp_other
val profileImage = view.image_gchat_profile_other
val user = view.text_gchat_user_other
fun bindView(context: Context, message: UserMessage) {
messageText.setText(message.message)
timestamp.text = DateUtil.formatTime(message.createdAt)
date.visibility = View.VISIBLE
date.text = DateUtil.formatDate(message.createdAt)
Glide.with(context).load(message.sender.profileUrl).apply(RequestOptions().override(75, 75))
.into(profileImage)
user.text = message.sender.nickname
}
}

For the sake of a cleaner look, we also added an object that has two functions that help with date formatting. This is the gist:

object DateUtil {
fun formatTime(timeInMillis: Long): String {
val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
return dateFormat.format(timeInMillis)
}
fun formatDate(timeInMillis: Long): String {
val dateFormat = SimpleDateFormat("MMMM dd", Locale.getDefault())
return dateFormat.format(timeInMillis)
}
}
view raw DateUtil.kt hosted with ❤ by GitHub

This completes the code for the MessageAdapter class. To see the completed class for MessageAdapter.kt, go here.

Step 3. The ChannelActivity.kt class

Now that we have implemented the UI and taken care of the Adapter to connect the UI to the passed data, we will implement the ChannelActivity.kt.

The following class will:

  • Set up the recyclerView and Adapter
  • Handle getting and entering the channel passed by either CreateChannelActivity or ChannelListActivity
  • Handle sending and receiving messages

First, create a ChannelActivity.kt class. In the onCreate function, we will set the contentView and call two functions to handle setting up.

onCreate

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat)
setUpRecyclerView()
setButtonHandlers()
}
view raw onCreate.kt hosted with ❤ by GitHub

The first function sets up the recyclerView and the messageAdapter we just created. This code follows a basic implementation for instantiating a recyclerView. Be sure to pass the context of the Activity for the messageAdapter as we will need that when setting the image, as we did above.

setUpRecyclerView.kt

/**
* Set up the recyclerview and set the adapter
*/
private fun setUpRecyclerView() {
adapter = MessageAdapter(this)
recyclerView = recycler_gchat
recyclerView.adapter = adapter
val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
layoutManager.reverseLayout = true
recyclerView.layoutManager = layoutManager
recyclerView.scrollToPosition(0)
}

The second function handles setting the two buttons on the Activity: Back and Send. The Back button is self-explanatory, so let’s focus on the Send button.

setButtonListeners

/**
* Function handles setting handlers for back/send button
*/
private fun setButtonListeners() {
val back = button_gchat_back
back.setOnClickListener {
val intent = Intent(this, ChannelListActivity::class.java)
startActivity(intent)
}
val send = button_gchat_send
send.setOnClickListener {
sendMessage()
}
}

For the Send button, we have a method called sendMessage. This function takes the text from the editText, and sets it on the param of UserMessageParams(). There is a lot more you can do with UserMessageParams(), and I encourage you to check it out, but for the sake of simplicity, we will just add the message. Then we’ll take the groupChannel instance and sendMessage(). Upon the return that it was successfully sent, we will add it to the adapter, then clear the editText.

sendMessage

/**
* Sends the message from the edit text, and clears text field.
*/
private fun sendMessage()
{
val params = UserMessageParams()
.setMessage(edit_gchat_message.text.toString())
groupChannel.sendUserMessage(params,
SendUserMessageHandler { userMessage, e ->
if (e != null) { // Error.
return@SendUserMessageHandler
}
adapter.addFirst(userMessage)
edit_gchat_message.text.clear()
})
}
view raw sendMessage.kt hosted with ❤ by GitHub

Now that we have taken the case of the onCreate call, let’s get to the onResume call. This call is where we will handle getting the passed channel, and where we will register the channel handler to get various events – in this case, the onMessageReceived event.

First, we will want to get the channelURL from the intent. We will move this to a separate method for sake of visibility.

getChannelUrl

/**
* Get the Channel URL from the passed intent
*/
private fun getChannelURl(): String {
val intent = this.intent
return intent.getStringExtra(EXTRA_CHANNEL_URL)
}

After we have the channelURL, we will need to make a call with the function GroupChannel.getChannel(). This call will take the channelUrl and retrieve the channel object to send messages and get relevant channel information. Once it returns successfully the channel, set the channel and be sure to call getMessages().

This function is pretty straightforward. It creates a previousMessageListQuery (There is a lot of customizing you can do here as well), then loads the messages, and finally calls loadMessages on the Adapter. This gets all the previous messages in the conversation. This function does support pagination so you can get the entire conversation history.

getMessages()

/**
* Function to get previous messages in channel
*/
private fun getMessages() {
val previousMessageListQuery = groupChannel.createPreviousMessageListQuery()
previousMessageListQuery.load(
100,
true,
object : PreviousMessageListQuery.MessageListQueryResult {
override fun onResult(
messages: MutableList<BaseMessage>?,
e: SendBirdException?
) {
if (e != null) {
Log.e("Error", e.message)
}
adapter.loadMessages(messages!!)
}
})
}
view raw getMessages.kt hosted with ❤ by GitHub

The final thing we need to do in the onResume call is to set a channel handler. The channel handler is how you can get various events such as typing indicators, message read, message delivered events, and onMessageReceived events. The only method we implemented was onMessageReceived. This event fires every time a message comes in from another user. Once we get the event, we add it to the Adapter as described in the following code.

onResume()

override fun onResume() {
super.onResume()
channelUrl = getChannelURl()
GroupChannel.getChannel(channelUrl,
GroupChannelGetHandler { groupChannel, e ->
if (e != null) {
// Error!
e.printStackTrace()
return@GroupChannelGetHandler
}
this.groupChannel = groupChannel
getMessages()
})
SendBird.addChannelHandler(
CHANNEL_HANDLER_ID,
object : ChannelHandler() {
override fun onMessageReceived(
baseChannel: BaseChannel,
baseMessage: BaseMessage
){
if (baseChannel.url == channelUrl) {
// Add new message to view
adapter.addFirst(baseMessage)
groupChannel.markAsRead()
}
}
})
}
view raw onResume.kt hosted with ❤ by GitHub

Since we had an onResume call, let’s add an onPause. The only thing we will do here is to remove the channel handler for clean-up.

onPause()

override fun onPause() {
super.onPause()
SendBird.removeChannelHandler(CHANNEL_HANDLER_ID)
}
view raw onPause.kt hosted with ❤ by GitHub

That completes the code for the ChannelActivity class. See the completed class for ChannelActivity.kt here.

Conclusion

In this tutorial, we covered how to:

  • create a chat UI
  • hook up that chat UI to be able to send/receive and display messages

This tutorial is a stepping stone to the many things you can do with the Sendbird SDK. With what we have just implemented, you can easily start to:

  • incorporate additional types of messages
  • add a more complete view to the message life cycle
  • add push notifications, typing indicators, translations, and so much more

You did it! You are now connecting users with chat in your Android app! Check our docs and developer portal to build more features. If you have questions, contact us on the community site. In the meantime, happy chat building! 🙂

U Ikit Mobile content offer background

Build in-app messaging with developer-friendly SDKs, APIs, and UIKits.

Need more resources?