Skip to main content
Version: v6

Customizing Message Composer

note

You can find the full code from this guide on GitHub. To check the final result, clone the repository, select the stream-chat-android-ui-guides module on your Android Studio like the image below, and run the module. UI Guides Module on Android Studio

If the built-in Message Composer View and its available customization options don't fit your app's needs, you can create a Message Composer View of your own.

Note that the UI Components Message Composer View supports many advanced features that you'll otherwise have to implement yourself if you want to use them in your app:

  • Sending and editing messages
  • Handling threads and replies
  • Supporting typing indicators
  • Browsing for and adding image and file attachments
  • Input validation such as a max length
  • Commands and mentions

With that, let's see how you can build a custom Message Composer View from scratch.

note

This sample is meant to be as simple as possible. You might want to architect your actual custom views in more advanced ways than shown here.

Creating a Layout

For this example, you'll create a custom View that extends ConstraintLayout. It'll inflate the following layout internally, which consists of a simple EditText and a Button.

<?xml version="1.0" encoding="utf-8"?>
<merge 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:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

<EditText
android:id="@+id/inputField"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/sendButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</merge>

Create a new class called CustomMessageComposerView, extending ConstraintLayout and adding the necessary basic View constructors. You can use View Binding to inflate the layout created above and easily access the Views it contains.

class CustomMessageComposerView : ConstraintLayout {

private val binding = ViewCustomMessageComposerBinding.inflate(LayoutInflater.from(context), this)

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

You can add this custom Message Composer View to your layout like so, combining it with other UI Components:

<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="match_parent">

<io.getstream.chat.android.ui.feature.messages.list.MessageListView
android:id="@+id/messageListView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/customMessageComposerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.example.CustomMessageComposerView
android:id="@+id/customMessageComposerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Running the app now shows the new Message Composer View, which you can freely style to fit your app's requirements.

Custom message composer

Sending and Editing Messages

Let's add support for sending and editing messages:

class CustomMessageComposerView : ConstraintLayout {

private val binding = ViewCustomMessageComposerBinding.inflate(LayoutInflater.from(context), this)

private lateinit var channelClient: ChannelClient

private var messageToEdit: Message? = null

constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

init {
// 1
binding.sendButton.setOnClickListener {
val text = binding.inputField.text.toString()

val messageToEdit = messageToEdit
if (messageToEdit != null) {
// 2
channelClient.updateMessage(messageToEdit.copy(text = text)).enqueue()
} else {
// 3
channelClient.sendMessage(Message(text = text, parentId = null)).enqueue()
}

// 4
this.messageToEdit = null
binding.inputField.setText("")
}
}

fun setChannelClient(channelClient: ChannelClient) {
this.channelClient = channelClient
}

// 5
fun editMessage(message: Message) {
this.messageToEdit = message
binding.inputField.setText(message.text)
}
}

In the snippet above, you:

  1. Set a click listener on the Send button.
  2. Read the current value from the input field.
  3. Send a new message or update the existing one.
  4. Clear the input for the next message.
  5. Add a method to allow editing existing messages.

Finally, initialize the composer with ChatClient and let the message composer know when we are editing a message:

customMessageComposerView.setChannelClient(ChatClient.instance().channel(cid))

messageListView.setMessageEditHandler(customMessageComposerView::editMessage)

Now you can send a message to the chat. Let's see how to add support for threads.

Handling Threads

Message List View has built-in support for threads, and your custom Message Composer View can be integrated with threads as well:

class CustomMessageComposerView : ConstraintLayout {

private val binding = ViewCustomMessageComposerBinding.inflate(LayoutInflater.from(context), this)

private lateinit var channelClient: ChannelClient

private var messageToEdit: Message? = null
private var parentMessage: Message? = null

constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

init {
binding.sendButton.setOnClickListener {
val text = binding.inputField.text.toString()

val messageToEdit = messageToEdit
if (messageToEdit != null) {
channelClient.updateMessage(messageToEdit.copy(text = text)).enqueue()
} else {
channelClient.sendMessage(Message(text = text, parentId = parentMessage?.id)).enqueue()
}

this.messageToEdit = null
binding.inputField.setText("")
}
}

fun setChannelClient(channelClient: ChannelClient) {
this.channelClient = channelClient
}

fun editMessage(message: Message) {
this.messageToEdit = message
binding.inputField.setText(message.text)
}

fun setActiveThread(parentMessage: Message) {
this.parentMessage = parentMessage
this.messageToEdit = null
binding.inputField.setText("")
}

fun resetThread() {
this.parentMessage = null
this.messageToEdit = null
binding.inputField.setText("")
}
}

Notice how the new parentMessage field is used to track if you are inside a thread. If that's the case, then you need to pass the parent message ID when sending a new message using the ChannelClient::sendMessage method.

To complete the feature, let the message composer know when we are entering or leaving a thread:

customMessageComposerView.setChannelClient(ChatClient.instance().channel(cid))

messageListView.setMessageEditHandler(customMessageComposerView::editMessage)

messageListViewModel.mode.observe(viewLifecycleOwner) { mode ->
when (mode) {
is MessageMode.MessageThread -> {
customMessageComposerView.setActiveThread(mode.parentMessage)
}
is MessageMode.Normal -> {
customMessageComposerView.resetThread()
}
}
}

Did you find this page helpful?