# Creating a Custom Message Input View

If the built-in [Message Input View](/chat/docs/sdk/android/v5/ui/message-components/message-input/) and its available customization options don't fit your app's needs, you can create an Input View of your own.

Note that the UI Components Input 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 an Input View from scratch.

<admonition type="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.

</admonition>

### 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
<?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"
        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:text="Send"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</merge>
```

Create a new class called `CustomMessageInput`, extending `ConstraintLayout` and adding the necessary basic View constructors. You can use [View Binding](https://developer.android.com/topic/libraries/view-binding) to inflate the layout created above and easily access the Views it contains.

```kotlin
class CustomMessageInput : ConstraintLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    val binding = CustomMessageInputBinding.inflate(LayoutInflater.from(context), this)
}
```

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

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout>

    <io.getstream.chat.android.ui.message.list.header.MessageListHeaderView
        android:id="@+id/messageListHeaderView"/>

    <io.getstream.chat.android.ui.message.list.MessageListView
        android:id="@+id/messageListView" />

    <com.example.chattutorial.custominput.CustomMessageInput
        android:id="@+id/messageInputView"
        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 input view, which you can freely style to fit your app's requirements.

![The message list screen with a custom input view](@chat-sdk/android/v5/_assets/custom-message-input-layout.png)

### Sending Messages

Time to add basic functionality to the layout.

This requires two new properties in the View. First, it needs the `cid` of the channel that the message has to be sent to. Secondly, it needs a reference to `ChatClient` to perform calls via the Stream SDK.

The `cid` can't be passed in as a constructor parameter, so you'll use a `lateinit var` for it. Be careful to always set this before attempting to set a message.

```kotlin
lateinit var cid: String
```

The `ChatClient` can always be obtained via the `instance()` method after it's been initialized in your app:

```kotlin
val chatClient = ChatClient.instance()
```

Finally, you need to set a listener on the _Send_ button to read the current input and send a message. Here's all the new code together:

```kotlin
class CustomMessageInput : ConstraintLayout {
    /* Constructors, binding ... */

    lateinit var cid: String
    val chatClient = ChatClient.instance()

    init {
        binding.sendButton.setOnClickListener {
            // 1
            val text = binding.inputField.text.toString()
            // 2
            val message = Message(cid = cid, text = text)
            // 3
            chatClient.channel(cid).sendMessage(message).enqueue { result ->
                if (result.isSuccess) {
                    // Yay!
                } else {
                    Toast.makeText(context, "Failed to send message", Toast.LENGTH_SHORT).show()
                }
            }
            // 4
            binding.inputField.setText("")
        }
    }
}
```

Here's what you're doing here, step-by-step:

1. Reading the current value from the `inputField`.
2. Creating the `Message` object to send, passing in the `cid` of the current channel and the `text` that it should contain.
3. Using `ChatClient` to send a message, with some basic error handling added. Note that `ChatClient::channel` returns a `ChannelClient` that simplifies performing operation in a particular channel.
4. Clearing the input for the next message.

Finally, don't forget to set the `cid` after the view is created, like so:

```kotlin
binding.messageInputView.cid = cid
```

### Supporting Typing Indicators

Stream uses events for indicating when a user starts or stop typing in a channel. In a custom Input View, you'll have to send these events if you want to support typing indicators in your app.

First, create a `Runnable` object within the View that will send a the "typing stopped" event when called:

```kotlin
val stopTyping: Runnable = Runnable {
    chatClient.channel(cid).stopTyping(parentId = null).enqueue()
}
```

Note that `enqueue` is called without a callback, so you're not checking whether or not the operation succeeded or what its result was. This is simplification as these events are not usually high priority.

<admonition type="note">

The `parentId` here would be the ID of the parent message in case you're typing in a thread (see below about supporting threading).

</admonition>

Then, inside the `init` block, add a `TextWatcher` to the `inputField` that will detect changes in the text input:

```kotlin
binding.inputField.addTextChangedListener(object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    override fun afterTextChanged(s: Editable?) {
        // 1
        if (s.isNullOrEmpty()) {
            post(stopTyping)
            return
        }

        // 2
        chatClient.channel(cid).keystroke(parentId = null).enqueue()

        // 3
        removeCallbacks(stopTyping)
        postDelayed(stopTyping, 5000L)
    }
})
```

In this snippet, you:

1. Check if the input is now empty, in this case you can immediately send a "typing stopped" event.
2. Send a keystroke event.
3. Remove all pending 'typing stopped events', and schedule a new one to be sent in a couple seconds from now.

You can use more advanced logic for sending these events in your own application, this is a basic example.

### Editing Messages

Stream allows users to edit existing messages. By default, this can be triggered from the [Message List component](/chat/docs/sdk/android/v5/ui/message-components/message-list/), by long tapping a message to access various actions.

To support this, you'll need to store the message being edited and add a new method that lets users set this property:

```kotlin
var messageToEdit: Message? = null

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

The input field is also updated here to reflect the original text of the message.

<admonition type="note">

You might want to add additional UI and listeners to cancel a pending edit and clear this value. You could also update the text / icon of the _Send_ button when a message is being edited.

</admonition>

Next, the listener of the _Send_ button has to be updated:

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

    val toEdit = messageToEdit
    if (toEdit != null) {
        // 1
        val message = toEdit.copy(text = text)
        chatClient.channel(cid).updateMessage(message).enqueue { result ->
            if (result.isError) {
                Toast.makeText(context, "Failed to edit message", Toast.LENGTH_SHORT).show()
            }
        }
    } else {
        // 2
        val message = Message(cid = cid, text = text)
        chatClient.channel(cid).sendMessage(message).enqueue { /* ... */ }
    }

    // 3
    binding.inputField.setText("")
    messageToEdit = null
}
```

1. Editing a message is done by copying the original message and updating its text, then using the `updateMessage` method on `ChatClient`.
2. Sending a new message is the same as before.
3. As a last step, you clear the message under edit.

Finally, connect the Input View to the Message List to trigger edits from the list:

```kotlin
binding.messageListView.setMessageEditHandler(binding.messageInputView::editMessage)
```

### Handling Threads

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

For this, you'll need to keep track of an additional value within the view: the current parent message.

```kotlin
var parentMessage: Message? = null
```

Add a pair of methods to be able to update the value of this property:

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

    this.parentMessage = parentMessage
}

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

    parentMessage = null
}
```

When entering or exiting a thread, it's a good idea to clear the current input as well as any pending edits.

To send a message to a thread, modify the sending code like so:

```kotlin
val message = Message(cid = cid, text = text)
parentMessage?.let { parent ->
    message.parentId = parent.id
}
chatClient.channel(cid).sendMessage(message).enqueue { /* ... */ }
```

This sets the `parentId` value on the newly created message if there is a parent message available.

<admonition type="note">

Another message property related to threading is `showInChannel`. Setting this to `true` will show a message sent to a thread in the main channel as well. You can add additional UI to let users choose this option, for example, the UI Components message input implementation shows a checkbox for this.

</admonition>

To connect this with the thread handling of the Message List View, add the following calls in your screen's implementation (this assumes that you're also using the default Message List Header View):

```kotlin
messageListViewModel.mode.observe(this) { mode ->
    when (mode) {
        is Thread -> {
            messageListHeaderViewModel.setActiveThread(mode.parentMessage)
            binding.messageInputView.setActiveThread(mode.parentMessage)
        }
        Normal -> {
            messageListHeaderViewModel.resetThread()
            binding.messageInputView.resetThread()
        }
    }
}
```

Finally, if you've added typing event support, make sure to update both the `keystroke` and `stopTyping` calls to include the parent ID now that threading is supported:

```kotlin
chatClient.channel(cid).keystroke(parentId = parentMessage?.id).enqueue()
chatClient.channel(cid).stopTyping(parentId = parentMessage?.id).enqueue()
```

### Full Source Code

Here's the full source code of a custom message input view that includes all of the features from above:

```kotlin
class CustomMessageInput : ConstraintLayout {

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

    val binding = CustomMessageInputBinding.inflate(LayoutInflater.from(context), this)
    lateinit var cid: String
    val chatClient = ChatClient.instance()

    val stopTyping: Runnable = Runnable {
        chatClient.channel(cid).stopTyping(cid = cid, parentId = parentMessage?.id).enqueue()
    }

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

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

            val toEdit = messageToEdit
            if (toEdit != null) {
                val message = toEdit.copy(text = text)
                chatClient.channel(cid).updateMessage(message).enqueue { result ->
                    if (result.isError) {
                        Toast.makeText(context, "Failed to edit message", Toast.LENGTH_SHORT).show()
                    }
                }
            } else {
                val message = Message(cid = cid, text = text)
                parentMessage?.let { parent ->
                    message.parentId = parent.id
                    message.showInChannel
                }
                chatClient.channel(cid).sendMessage(message).enqueue { result ->
                    if (result.isSuccess) {
                        // Yay!
                    } else {
                        Toast.makeText(context, "Failed to send message", Toast.LENGTH_SHORT).show()
                    }
                }
            }

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

        binding.inputField.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
            override fun afterTextChanged(s: Editable?) {
                if (s.isNullOrEmpty()) {
                    post(stopTyping)
                    return
                }

                chatClient.channel(cid).keystroke(parentId = parentMessage?.id).enqueue()

                removeCallbacks(stopTyping)
                postDelayed(stopTyping, 5000L)
            }
        })
    }

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

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

        parentMessage = null
    }

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

        this.parentMessage = parentMessage
    }

}
```


---

This page was last updated at 2026-05-13T13:38:35.981Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/v5/ui/guides/custom-message-input/](https://getstream.io/chat/docs/sdk/android/v5/ui/guides/custom-message-input/).