Skip to main content
A MessageTemplate defines and customizes both the structure and the behavior of the MessageBubble. It acts as a schema for creating MessageBubble components, allowing you to manage the appearance and interactions of message bubbles within your application.

When to Use This

  • You need to customize the header, content, footer, bottom, or status info view of a message bubble
  • You want to replace the default bubble layout with a completely custom view
  • You need to add or modify the long-press options on a message bubble
  • You want to create a new template for a custom message type (e.g., a contact card)
  • You need to change how a specific message type (text, image, etc.) renders in the MessageList

Prerequisites

  • CometChat Android UI Kit dependency added to your project
  • CometChatUIKit.init() called and completed
  • A logged-in CometChat user
  • Familiarity with the MessageList component
  • Familiarity with MessageBubble styling

Quick Start

  1. Get the list of existing message templates from the data source:
val messageTemplates = CometChatUIKit.dataSource.messageTemplates(messagelist.getAddtionalParameters())
What this does: Retrieves all registered message templates from the UI Kit data source, giving you the full list of templates to modify or extend.
  1. Find the template for the message type you want to customize (e.g., text messages):
for (template in messageTemplates) {
    if (template.type == UIKitConstants.MessageType.TEXT) {
        // Code to customize text message template
    }
}
What this does: Iterates through the templates list and matches the template whose type equals UIKitConstants.MessageType.TEXT, so you can customize only text message bubbles.
  1. Customize a view on the matched template (e.g., set a custom content view):
template.setContentView(object : MessagesViewHolderListener {
    override fun createView(
        context: Context,
        cometChatMessageBubble: CometChatMessageBubble,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
    ): View {
        return LayoutInflater.from(context).inflate(R.layout.your_custom_layout, null)
    }

    override fun bindView(
        context: Context,
        view: View,
        baseMessage: BaseMessage,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
        viewHolder: RecyclerView.ViewHolder,
        list: List<BaseMessage>,
        i: Int
    ) {
        // Bind your custom view data here
    }
})
What this does: Overrides the content view of the matched template by providing a MessagesViewHolderListener that inflates a custom layout in createView() and binds message data to it in bindView().
  1. Apply the modified templates to the MessageList component:
messageList.setTemplates(messageTemplates)
What this does: Passes the modified templates list to the MessageList component so it uses your customized templates when rendering message bubbles.

Core Concepts

MessageBubble Structure

The MessageBubble structure is broken down into these views:
  1. Leading view: Displays the sender’s avatar. It appears on the left of the MessageBubble for messages from others and on the right for messages from the current user.
  2. Header view: Displays the sender’s name. This is useful in group chats where multiple users are sending messages.
  3. Content view: The core of the MessageBubble where the message content (text, images, videos, etc.) is displayed.
  4. Bottom view: Extends the MessageBubble with additional elements, such as link previews or a “load more” button for long messages. It is placed beneath the Content view.
  5. Footer view: Displays the timestamp of the message and its delivery or read status. It is located at the bottom of the MessageBubble.

Template Properties

MessageTemplate provides methods that allow you to alter various properties of the MessageBubble, including the type and category of a message, and the appearance and behavior of the header, content, and footer sections. Type: Use setType() to set the type of CometChatMessage. This maps your MessageTemplate to the corresponding CometChatMessage.
messageTemplate.setType(UIKitConstants.MessageType.CUSTOM);
What this does: Sets the message type on the template so the UI Kit knows which incoming messages this template applies to.
Category: Use setCategory() to set the category of a MessageTemplate. This creates a MessageTemplate with the specified category and links it with a CometChatMessage of the same category. Refer to the guide on Message Categories for a deeper understanding.
messageTemplate.setCategory(UIKitConstants.MessageCategory.CUSTOM);
What this does: Sets the message category on the template, linking it to messages of the same category (e.g., CUSTOM).

Implementation

Header View

What you are changing: The header area of the message bubble, which displays above the content view.
  • Where: MessagesViewHolderListener passed to template.setHeaderView()
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.TEXT)
  • Default behavior: Displays the sender’s name
  • Override: Pass a MessagesViewHolderListener to setHeaderView() that inflates a custom layout in createView() and binds data in bindView()
XML Layout:
message_template_header_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/name_with_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="George Alan • 🗓️ In meeting"
        android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
        android:textColor="?attr/cometchatPrimaryColor" />

</LinearLayout>
What this does: Defines a custom header layout with a single TextView that shows the sender’s name and a status indicator.
Code:
template.setHeaderView(object : MessagesViewHolderListener {
    override fun createView(
        context: Context,
        cometChatMessageBubble: CometChatMessageBubble,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
    ): View {
        return layoutInflater.inflate(R.layout.message_template_header_view, null)
    }

    override fun bindView(
        context: Context,
        view: View,
        baseMessage: BaseMessage,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
        viewHolder: RecyclerView.ViewHolder,
        list: List<BaseMessage>,
        i: Int
    ) {
        val textView = view.findViewById<TextView>(R.id.name_with_status)
        textView.setText(message.getSender().getName() + " • " + "\uD83D\uDDD3\uFE0F In meeting")
    }
})
What this does: The createView() method inflates message_template_header_view.xml as the header view for every message. The bindView() method sets the sender’s name and a status emoji on the TextView, and is called every time a ViewHolder for that message type is bound.
  • Verify: Each text message bubble displays a custom header showing the sender’s name followed by ” • 🗓️ In meeting” in the primary color, replacing the default sender name header.

Content View

What you are changing: The main content area of the message bubble where the message body is displayed.
  • Where: MessagesViewHolderListener passed to template.setContentView()
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.IMAGE)
  • Default behavior: Displays the Text Bubble, Image Bubble, File Bubble, Audio Bubble, or Video Bubble, depending on the message type
  • Override: Pass a MessagesViewHolderListener to setContentView() that inflates a custom layout in createView() and binds data in bindView()
XML Layout:
image_bubble_content_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/image_bubble_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.cometchat.chatuikit.shared.views.cometchatimagebubble.CometChatImageBubble
            android:id="@+id/imageBubble"
            android:layout_width="@dimen/cometchat_240dp"
            android:layout_height="@dimen/cometchat_232dp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/cometchat_margin_2"
            android:text="Buy Now"
            android:textAlignment="center"
            android:textAppearance="?attr/cometchatTextAppearanceButtonMedium"
            android:textColor="?attr/cometchatStrokeColorHighlight" />
    </LinearLayout>

    <com.cometchat.chatuikit.shared.views.deletebubble.CometChatDeleteBubble
        android:id="@+id/cometchat_delete_text_bubble"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone" />
</LinearLayout>
What this does: Defines a custom content layout with a CometChatImageBubble, a “Buy Now” label below it, and a hidden CometChatDeleteBubble that appears when the message is deleted.
Code:
val templates = ChatConfigurator.getDataSource().getMessageTemplates(messageList.additionParameter)

        for (template in templates) {
            if (template.type == UIKitConstants.MessageType.IMAGE) {
                template.setContentView(object : MessagesViewHolderListener() {
                    override fun createView(
                        context: Context,
                        cometChatMessageBubble: CometChatMessageBubble,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
                    ): View {
                        return LayoutInflater.from(context).inflate(R.layout.image_bubble_content_view, null)
                    }

                    override fun bindView(
                        context: Context,
                        view: View,
                        baseMessage: BaseMessage,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
                        viewHolder: RecyclerView.ViewHolder,
                        list: List<BaseMessage>,
                        i: Int
                    ) {
                        if (view != null) {
                            val deleteStyle: Int
                            val bubbleStyle: Int

                            if (messageBubbleAlignment == UIKitConstants.MessageBubbleAlignment.RIGHT) {
                                deleteStyle = com.cometchat.chatuikit.R.style.CometChatIncomingMessageDeleteStyle
                                bubbleStyle = com.cometchat.chatuikit.R.style.CometChatIncomingImageMessageBubbleStyle
                            } else {
                                deleteStyle = com.cometchat.chatuikit.R.style.CometChatOutgoingDeleteBubbleStyle
                                bubbleStyle = com.cometchat.chatuikit.R.style.CometChatOutgoingImageBubbleStyle
                            }

                            val linearLayout = view.findViewById<LinearLayout>(R.id.image_bubble_container)
                            val cometchatImageBubble = view.findViewById<CometChatImageBubble>(R.id.imageBubble)
                            val deletedBubble = view.findViewById<CometChatDeleteBubble>(com.cometchat.chatuikit.R.id.cometchat_delete_text_bubble)
                            val mediaMessage = baseMessage as MediaMessage
                            if (mediaMessage.deletedAt == 0L) {
                                cometchatImageBubble.style = bubbleStyle
                                deletedBubble.visibility = View.GONE
                                cometchatImageBubble.visibility = View.VISIBLE
                                val attachment = mediaMessage.attachment
                                val file = Utils.getFileFromLocalPath(mediaMessage)
                                cometchatImageBubble.setImageUrl(
                                    file,
                                    if (attachment != null) attachment.fileUrl else "",
                                    attachment?.fileExtension?.equals("gif", ignoreCase = true) ?: Utils.isGifFile(file)
                                )
                            } else {
                                linearLayout.visibility = View.GONE
                                deletedBubble.visibility = View.VISIBLE
                                deletedBubble.style = deleteStyle
                            }
                        }
                    }
                })
                break
            }
        }
messageList.setTemplates(templates)
What this does: The createView() method inflates image_bubble_content_view.xml as the content view for every image message. The bindView() method handles displaying the image with CometChatImageBubble when the message is not deleted, and showing a CometChatDeleteBubble when the message has been deleted.
  • Verify: Image message bubbles display a CometChatImageBubble with a “Buy Now” label below it. If the message is deleted, the image is hidden and a delete bubble appears instead.

Status Info View

What you are changing: The status info area inside the message bubble, which displays delivery/read status indicators.
  • Where: MessagesViewHolderListener passed to template.setStatusInfoView() (and optionally template.setFooterView() for relocated status content)
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.TEXT)
  • Default behavior: Displays the message receipt and timestamp inside the bubble
  • Override: Pass a MessagesViewHolderListener to setStatusInfoView() that returns a minimal empty view, and move the status content to the footer view using setFooterView()
XML Layout:
status_info_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end"
    android:layout_marginStart="@dimen/cometchat_100dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/cometchat_8dp"
        android:layout_marginBottom="@dimen/cometchat_4dp"
        android:text="12:00 pm"
        android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
        android:textColor="?attr/cometchatTextColorSecondary" />

    <com.cometchat.chatuikit.shared.views.cometchatmessagereceipt.CometChatMessageReceipt
        android:id="@+id/receipt"
        android:layout_width="@dimen/cometchat_17dp"
        android:layout_height="@dimen/cometchat_17dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="@dimen/cometchat_8dp"
        android:layout_marginBottom="@dimen/cometchat_4dp" />
</LinearLayout>
What this does: Defines a custom status info layout with a time TextView and a CometChatMessageReceipt view arranged horizontally, aligned to the end of the bubble.
Code:
    val templates = ChatConfigurator.getDataSource().getMessageTemplates(messageList.additionParameter)

        for (template in templates) {
            if (template.type == UIKitConstants.MessageType.TEXT) {
                template.setStatusInfoView(object : MessagesViewHolderListener() {
                    override fun createView(
                        context: Context,
                        cometChatMessageBubble: CometChatMessageBubble,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
                    ): View {
                        val view = View(context)
                        val layoutParams = LinearLayout.LayoutParams(
                            context.resources.getDimensionPixelSize(com.cometchat.chatuikit.R.dimen.cometchat_1dp),
                            context.resources.getDimensionPixelSize(com.cometchat.chatuikit.R.dimen.cometchat_12dp)
                        )
                        view.layoutParams = layoutParams
                        return view
                    }

                    override fun bindView(
                        context: Context,
                        view: View,
                        baseMessage: BaseMessage,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
                        viewHolder: RecyclerView.ViewHolder,
                        list: List<BaseMessage>,
                        i: Int
                    ) {
                    }
                })

                template.setFooterView(object : MessagesViewHolderListener() {
                    override fun createView(
                        context: Context,
                        cometChatMessageBubble: CometChatMessageBubble,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
                    ): View {
                        return LayoutInflater.from(context).inflate(R.layout.status_info_layout, null)
                    }

                    override fun bindView(
                        context: Context,
                        createdView: View,
                        baseMessage: BaseMessage,
                        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
                        viewHolder: RecyclerView.ViewHolder,
                        list: List<BaseMessage>,
                        i: Int
                    ) {
                        val tvTime = createdView.findViewById<TextView>(R.id.time)
                        val receipt = createdView.findViewById<CometChatMessageReceipt>(R.id.receipt)
                        if (messageBubbleAlignment == UIKitConstants.MessageBubbleAlignment.RIGHT) {
                            receipt.visibility = View.VISIBLE
                            receipt.setMessageReceipt(MessageReceiptUtils.MessageReceipt(baseMessage))
                        } else {
                            receipt.visibility = View.GONE
                        }
                        tvTime.text = SimpleDateFormat("hh:mm a").format(baseMessage.sentAt * 1000)
                    }
                })
                break
            }
        }
    messageList.setTemplates(templates)
What this does: The setStatusInfoView() replaces the default in-bubble status info with a minimal 1dp×12dp empty view. The setFooterView() inflates status_info_layout.xml as the footer, displaying the timestamp and message receipt outside the bubble. If the alignment is RIGHT (sent messages), the receipt icon is visible; if LEFT (received messages), the receipt icon is hidden.
  • Verify: Text message bubbles show the timestamp and delivery receipt below the bubble (in the footer area) instead of inside the bubble. Sent messages display both the time and receipt icon; received messages display only the time.

Bottom View

What you are changing: The bottom area of the message bubble, placed beneath the content view.
  • Where: MessagesViewHolderListener passed to template.setBottomView()
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.TEXT)
  • Default behavior: Displays buttons such as link previews or a “load more” button for long messages
  • Override: Pass a MessagesViewHolderListener to setBottomView() that inflates a custom layout in createView() and binds data in bindView()
XML Layout:
message_template_bottom_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:background="#FFFFFF"
    android:gravity="center_vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#33F44649"
        android:orientation="horizontal"
        android:padding="2dp">

        <ImageView
            android:id="@+id/backIcon"
            android:layout_width="@dimen/cometchat_12dp"
            android:layout_height="@dimen/cometchat_12dp"
            android:layout_marginStart="8dp"
            android:src="@drawable/cometchat_ic_message_error"
            app:tint="?attr/cometchatErrorColor" />

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:text="According to guidelines you cannot share contact"
            android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
            android:textColor="#F44649"
            android:translationZ="1dp" />
    </LinearLayout>

</LinearLayout>
What this does: Defines a custom bottom view layout with an error icon and a red warning text, used to display a policy warning below the message content.
Code:
template.setBottomView(object : MessagesViewHolderListener {
    override fun createView(
        context: Context,
        cometChatMessageBubble: CometChatMessageBubble,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
    ): View {
        return LayoutInflater.from(context).inflate(R.layout.message_template_bottom_view, null)
    }

    override fun bindView(
        context: Context,
        view: View,
        baseMessage: BaseMessage,
        messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment,
        viewHolder: RecyclerView.ViewHolder,
        list: List<BaseMessage>,
        i: Int
    ) {
        createdView.setVisibility(View.GONE);
        if (baseMessage.getMetadata() != null && MessageReceiptUtils.MessageReceipt(baseMessage).equals(Receipt.ERROR)) {
            createdView.setVisibility(View.VISIBLE);
        }
    }
})
What this does: The createView() method inflates message_template_bottom_view.xml as the bottom view for every message. The bindView() method hides the bottom view by default and shows it only if the message has metadata and the message receipt equals Receipt.ERROR.
  • Verify: The bottom view is hidden by default. If a message has an error receipt (Receipt.ERROR) and non-null metadata, a red warning banner with an error icon and text appears below the message content.
What you are changing: The footer area of the message bubble, located at the bottom of the bubble.
  • Where: MessagesViewHolderListener passed to template.setFooterView()
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.TEXT)
  • Default behavior: Displays the receipt and timestamp
  • Override: Pass a MessagesViewHolderListener to setFooterView() that inflates a custom layout in createView() and binds data in bindView()
XML Layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/cometchat_reaction_layout_parent_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?attr/cometchatStrokeColorLight" />

        <com.cometchat.chatuikit.extensions.reaction.CometChatMessageReaction
            android:id="@+id/cometchat_reaction_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</LinearLayout>
What this does: Defines a custom footer layout with a thin separator line and a CometChatMessageReaction view that displays message reactions.
Code:
template.setFooterView(object : MessagesViewHolderListener() {
            override fun createView(context: Context, messageBubble: CometChatMessageBubble, alignment: MessageBubbleAlignment): View {
                return LayoutInflater.from(context).inflate(R.layout.message_template_footer_view, null)
            }

            override fun bindView(
                context: Context,
                createdView: View,
                message: BaseMessage,
                alignment: MessageBubbleAlignment,
                holder: RecyclerView.ViewHolder,
                messageList: List<BaseMessage>,
                position: Int
            ) {
                val messageReaction = createdView.findViewById<CometChatMessageReaction>(R.id.cometchat_reaction_view)
                val reactionLayout = createdView.findViewById<LinearLayout>(R.id.cometchat_reaction_layout_parent_container)
                if (alignment == MessageBubbleAlignment.RIGHT) reactionLayout.setBackgroundColor(CometChatTheme.getPrimaryColor(context))
                else reactionLayout.setBackgroundColor(CometChatTheme.getNeutralColor300(context))

                messageReaction.setStyle(R.style.CometChatReactionStyle)
                messageReaction.bindReactionsToMessage(message, 4)
            }
        })
What this does: The createView() method inflates a custom footer layout with a CometChatMessageReaction view. The bindView() method sets the background color based on alignment (primary color for sent messages, neutral color for received messages), applies the CometChatReactionStyle, and binds up to 4 reactions to the message.
  • Verify: Each message bubble displays a reactions bar in the footer area with a separator line above it. Sent message footers use the primary color background; received message footers use the neutral color background. Up to 4 reactions are displayed per message.

Bubble View

What you are changing: The entire message bubble, replacing the default combination of header, content, and footer views with a fully custom layout.
  • Where: MessagesViewHolderListener passed to template.setBubbleView()
  • Applies to: Any message type matched by the template (e.g., UIKitConstants.MessageType.TEXT)
  • Default behavior: The headerView, contentView, and footerView together form a message bubble
  • Override: Pass a MessagesViewHolderListener to setBubbleView() that inflates a completely custom bubble layout in createView() and binds data in bindView()
XML Layouts: drawable/left_bubble_bg
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="@dimen/cometchat_100dp"
    android:height="@dimen/cometchat_45dp"
    android:viewportWidth="181"
    android:viewportHeight="65">
  <path
      android:pathData="M180.56,2.88C180.56,1.29 179.27,0 177.68,0H2.88C1.29,0 0,1.29 0,2.88V58.56H177.68C179.27,58.56 180.56,57.27 180.56,55.68V2.88Z"
      android:fillColor="?attr/cometchatNeutralColor300"/>
  <path
      android:pathData="M12.06,56.84C12.8,56.26 12.8,55.14 12.06,54.57L2.89,47.43C1.94,46.69 0.56,47.37 0.56,48.57L0.56,62.84C0.56,64.04 1.94,64.71 2.89,63.97L12.06,56.84Z"
      android:fillColor="?attr/cometchatNeutralColor300"/>
</vector>
What this does: Defines a vector drawable for the left (incoming) bubble background with a speech-bubble tail pointing left, using the neutral color.
drawable/right_bubble_bg
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="@dimen/cometchat_100dp"
    android:height="@dimen/cometchat_45dp"
    android:viewportWidth="181"
    android:viewportHeight="65">
  <path
      android:pathData="M0,2.88C0,1.29 1.29,0 2.88,0H177.68C179.27,0 180.56,1.29 180.56,2.88V58.56H2.88C1.29,58.56 0,57.27 0,55.68V2.88Z"
      android:fillColor="?attr/cometchatPrimaryColor"/>
  <path
      android:pathData="M168.5,56.84C167.76,56.26 167.76,55.14 168.5,54.57L177.68,47.43C178.62,46.69 180,47.37 180,48.57V62.84C180,64.04 178.62,64.71 177.68,63.97L168.5,56.84Z"
      android:fillColor="?attr/cometchatPrimaryColor"/>
</vector>
What this does: Defines a vector drawable for the right (outgoing) bubble background with a speech-bubble tail pointing right, using the primary color.
outgoing_text_bubble_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"
        android:layout_marginStart="@dimen/cometchat_100dp"
        android:background="@drawable/right_bubble_bg"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text_message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/cometchat_8dp"
            android:text="aefeafa"
            android:textColor="@color/white" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"
        android:layout_marginStart="@dimen/cometchat_100dp"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="12:00 pm"
            android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
            android:textColor="?attr/cometchatTextColorSecondary" />

        <com.cometchat.chatuikit.shared.views.cometchatmessagereceipt.CometChatMessageReceipt
            android:id="@+id/receipt"
            android:layout_width="@dimen/cometchat_17dp"
            android:layout_height="@dimen/cometchat_17dp"
            android:layout_marginStart="8dp" />


    </LinearLayout>
</LinearLayout>
What this does: Defines the outgoing (sent) text bubble layout with a right-aligned bubble background, a text message TextView, a timestamp, and a CometChatMessageReceipt view.
incoming_text_bubble_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/cometchat_100dp"
        android:background="@drawable/left_bubble_bg"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text_message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/cometchat_8dp"
            android:textAlignment="textStart"
            android:textColor="@color/black" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/cometchat_100dp"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="12:00 pm"
            android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
            android:textColor="?attr/cometchatTextColorSecondary" />

    </LinearLayout>
</LinearLayout>
What this does: Defines the incoming (received) text bubble layout with a left-aligned bubble background, a text message TextView, and a timestamp (no receipt icon for incoming messages).
Code:
template.setBubbleView(object : MessagesViewHolderListener() {
            override fun createView(
                context: Context,
                messageBubble: CometChatMessageBubble,
                alignment: MessageBubbleAlignment
            ): View {
                return if (alignment == MessageBubbleAlignment.LEFT) LayoutInflater.from(context).inflate(R.layout.incoming_text_bubble_view, null)
                else LayoutInflater.from(context).inflate(R.layout.outgoing_text_bubble_view, null)
            }

            override fun bindView(
                context: Context,
                createdView: View,
                message: BaseMessage,
                alignment: MessageBubbleAlignment,
                holder: RecyclerView.ViewHolder,
                messageList: List<BaseMessage>,
                position: Int
            ) {
                val textView = createdView.findViewById<TextView>(R.id.text_message)
                val tvTime = createdView.findViewById<TextView>(R.id.time)
                if (alignment == MessageBubbleAlignment.RIGHT) {
                    val receipt = createdView.findViewById<CometChatMessageReceipt>(R.id.receipt)
                    receipt.setMessageReceipt(MessageReceiptUtils.MessageReceipt(message))
                }

                val textMessage = message as TextMessage

                tvTime.text = SimpleDateFormat("hh:mm a").format(textMessage.sentAt * 1000)
                textView.text = textMessage.text
            }
        })
What this does: The createView() method inflates incoming_text_bubble_view.xml if the alignment is LEFT (received messages) or outgoing_text_bubble_view.xml if the alignment is RIGHT (sent messages). The bindView() method sets the message text, timestamp, and receipt icon (for sent messages only), completely replacing the default bubble structure.
  • Verify: Text messages display with custom speech-bubble-shaped backgrounds — outgoing messages use the primary color with a right-pointing tail, incoming messages use the neutral color with a left-pointing tail. Each bubble shows the message text, timestamp, and (for sent messages) a delivery receipt icon.

Options List

What you are changing: The list of actions that appear in the action sheet when a message is long-pressed.
  • Where: Lambda/callback passed to template.setOptions() or messageTemplate.setOptions()
  • Applies to: Any message type matched by the template
  • Default behavior: Displays a set of options like “Reply”, “Forward”, “Edit”, and “Delete”
  • Override: Pass a lambda to setOptions() that returns a custom list of CometChatMessageOption objects
Code:
 val messageTemplates = ChatConfigurator
            .getDataSource()
            .getMessageTemplates(messageList.additionParameter)

        for (messageTemplate in messageTemplates) {
            messageTemplate.setOptions { context: Context?, baseMessage: BaseMessage?, group: Group? ->
                val refreshOption =
                    CometChatMessageOption(
                        "REFRESH",
                        "Refresh",
                        android.R.drawable.ic_refresh
                    ) { Toast.makeText(context, "Refresh clicked", Toast.LENGTH_SHORT).show() }
                val options: MutableList<CometChatMessageOption> =
                    ArrayList()
                options.add(refreshOption)
                options.addAll(CometChatUIKit.getDataSource().getMessageOptions(context, baseMessage, group))
                options
            }
        }

messageList.setTemplates(messageTemplates)
What this does: Creates a custom “Refresh” option using CometChatMessageOption with an ID of "REFRESH", a label, an icon, and a click handler that shows a toast. This option is prepended to the default options list retrieved from CometChatUIKit.getDataSource().getMessageOptions(), so the “Refresh” option appears first in the long-press action sheet for all message types.
  • Verify: Long-pressing any message displays an action sheet with “Refresh” as the first option, followed by the default options (Reply, Forward, Edit, Delete, etc.). Tapping “Refresh” shows a toast message “Refresh clicked”.

New Template

What you are changing: Adding an entirely new message template for a custom message type that does not have a default template.
  • Where: Create a new CometChatMessageTemplate instance and add it to the templates list
  • Applies to: Custom message types (e.g., a “card” type with UIKitConstants.MessageCategory.CUSTOM)
  • Default behavior: Custom message types without a template are not rendered in the MessageList
  • Override: Create a new CometChatMessageTemplate, set its type and category, define a setBubbleView(), and add it to the templates list
Sending a custom message:
        val jsonObject = JSONObject()
        try {
            jsonObject.put("contact_name", "John Doe")
            jsonObject.put("contact_avatar", "https://img.freepik.com/free-vector/blue-circle-with-white-user_78370-4707.jpg?semt=ais_hybrid")
            jsonObject.put("contact_number", "+91 1234567890")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        val customMessage = CustomMessage(
            if (user != null) user!!.uid else group.getGuid(),
            if (user != null) CometChatConstants.RECEIVER_TYPE_USER else CometChatConstants.RECEIVER_TYPE_GROUP,
            "card",
            jsonObject
        )

        CometChatUIKit.sendCustomMessage(customMessage, object : CallbackListener<CustomMessage?>() {
            override fun onSuccess(customMessage: CustomMessage?) {
            }

            override fun onError(e: CometChatException?) {
            }
        })
What this does: Creates a CustomMessage with type "card" containing contact data (contact_name, contact_avatar, contact_number) as a JSONObject, and sends it using CometChatUIKit.sendCustomMessage().
XML Layout for the contact card:
contact_card.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:cardCornerRadius="@dimen/cometchat_12dp"
    app:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="@dimen/cometchat_240dp"
        android:layout_height="@dimen/cometchat_116dp"
        android:background="#6852D6"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/cometchat_margin_3"
            android:layout_marginTop="@dimen/cometchat_margin_3"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <com.cometchat.chatuikit.shared.views.cometchatavatar.CometChatAvatar
                android:id="@+id/avatar"
                android:layout_width="@dimen/cometchat_40dp"
                android:layout_height="@dimen/cometchat_40dp"
                app:cardCornerRadius="@dimen/cometchat_radius_max" />

            <TextView
                android:id="@+id/contactName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/cometchat_8dp"
                android:text="Safia Fareena"
                android:textAppearance="?attr/cometchatTextAppearanceBodyRegular"
                android:textColor="?attr/cometchatColorWhite" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_marginEnd="@dimen/cometchat_margin_3"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="12:00 pm"
                android:textAppearance="?attr/cometchatTextAppearanceCaption2Regular"
                android:textColor="?attr/cometchatColorWhite" />

            <com.cometchat.chatuikit.shared.views.cometchatmessagereceipt.CometChatMessageReceipt
                android:id="@+id/receipt"
                android:layout_width="@dimen/cometchat_17dp"
                android:layout_height="@dimen/cometchat_17dp"
                android:layout_marginStart="8dp" />

        </LinearLayout>

        <TextView
            android:id="@+id/separator"
            android:layout_width="match_parent"
            android:layout_height="@dimen/cometchat_1dp"
            android:layout_marginStart="@dimen/cometchat_margin_1"
            android:layout_marginTop="@dimen/cometchat_8dp"
            android:layout_marginEnd="@dimen/cometchat_margin_1"
            android:background="#7965DB" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/addContact"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:paddingStart="@dimen/cometchat_padding_5"
                android:paddingTop="@dimen/cometchat_padding_2"
                android:paddingEnd="@dimen/cometchat_padding_5"
                android:paddingBottom="@dimen/cometchat_padding_2"
                android:text="Add Contact"
                android:textAlignment="center"
                android:textAppearance="?attr/cometchatTextAppearanceButtonMedium"
                android:textColor="?attr/cometchatColorWhite" />

            <TextView
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#7965DB" />

            <TextView
                android:id="@+id/message"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:paddingStart="@dimen/cometchat_padding_5"
                android:paddingTop="@dimen/cometchat_padding_2"
                android:paddingEnd="@dimen/cometchat_padding_5"
                android:paddingBottom="@dimen/cometchat_padding_2"
                android:text="Message"
                android:textAlignment="center"
                android:textAppearance="?attr/cometchatTextAppearanceButtonMedium"
                android:textColor="?attr/cometchatColorWhite" />
        </LinearLayout>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>
What this does: Defines a contact card layout using MaterialCardView with a CometChatAvatar, contact name, timestamp, receipt icon, a separator, and two action buttons (“Add Contact” and “Message”).
Creating and registering the new template:
        val contactTemplate = CometChatMessageTemplate()
        contactTemplate.setType("card")
        contactTemplate.setCategory(UIKitConstants.MessageCategory.CUSTOM)
        contactTemplate.setBubbleView(object : MessagesViewHolderListener() {
            override fun createView(
                context: Context,
                cometChatMessageBubble: CometChatMessageBubble,
                messageBubbleAlignment: UIKitConstants.MessageBubbleAlignment
            ): View {
                return LayoutInflater.from(context).inflate(R.layout.contact_card, null)
            }

            override fun bindView(
                context: Context,
                view: View,
                baseMessage: BaseMessage,
                alignment: UIKitConstants.MessageBubbleAlignment,
                viewHolder: RecyclerView.ViewHolder,
                list: List<BaseMessage>,
                i: Int
            ) {
                val layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT
                )
                view.layoutParams = layoutParams
                val avatar = view.findViewById<CometChatAvatar>(R.id.avatar)
                val name = view.findViewById<TextView>(R.id.contactName)
                val time = view.findViewById<TextView>(R.id.time)
                val receipt = view.findViewById<CometChatMessageReceipt>(R.id.receipt)

                val customMessage = baseMessage as CustomMessage
                val jsonObject = customMessage.customData
                try {
                    name.text = jsonObject.getString("contact_name")
                    avatar.avatar = jsonObject.getString("contact_avatar")
                } catch (e: Exception) {
                    e.printStackTrace()
                }

                if (alignment == UIKitConstants.MessageBubbleAlignment.RIGHT) {
                    receipt.visibility = View.VISIBLE
                    receipt.setMessageReceipt(MessageReceiptUtils.MessageReceipt(baseMessage))
                } else {
                    receipt.visibility = View.GONE
                }

                time.text = SimpleDateFormat("hh:mm a").format(baseMessage.getSentAt() * 1000)
            }
        })

        messageTemplates.add(contactTemplate)
        messageList.setTemplates(messageTemplates)
What this does: Creates a new CometChatMessageTemplate with type "card" and category UIKitConstants.MessageCategory.CUSTOM. The setBubbleView() inflates contact_card.xml and binds the contact name, avatar, timestamp, and receipt from the CustomMessage’s customData JSON. The new template is added to the existing templates list and applied to the MessageList via setTemplates(). This renders the custom “card” message as a contact card in the chat.
  • Verify: Custom messages with type "card" appear in the MessageList as a purple contact card showing the contact’s avatar, name, timestamp, delivery receipt (for sent messages), and two action buttons (“Add Contact” and “Message”).

Customization Matrix

What you want to changeWhereProperty/APIExample
Message type mappingCometChatMessageTemplatesetType()messageTemplate.setType(UIKitConstants.MessageType.CUSTOM)
Message category mappingCometChatMessageTemplatesetCategory()messageTemplate.setCategory(UIKitConstants.MessageCategory.CUSTOM)
Header area (sender name)CometChatMessageTemplatesetHeaderView()Pass a MessagesViewHolderListener that inflates a custom header layout
Content area (message body)CometChatMessageTemplatesetContentView()Pass a MessagesViewHolderListener that inflates a custom content layout
Footer area (timestamp/receipt)CometChatMessageTemplatesetFooterView()Pass a MessagesViewHolderListener that inflates a custom footer layout
Bottom area (below content)CometChatMessageTemplatesetBottomView()Pass a MessagesViewHolderListener that inflates a custom bottom layout
Status info (in-bubble receipt)CometChatMessageTemplatesetStatusInfoView()Pass a MessagesViewHolderListener that returns a minimal or custom view
Entire bubble layoutCometChatMessageTemplatesetBubbleView()Pass a MessagesViewHolderListener that inflates a fully custom bubble layout
Long-press action optionsCometChatMessageTemplatesetOptions()Pass a lambda returning a list of CometChatMessageOption objects

Common Pitfalls and Fixes

PitfallFix
Modified templates but message bubbles do not changeCall messageList.setTemplates(messageTemplates) after modifying the templates. If you do not apply the templates to the MessageList, your changes have no effect.
Returning null from createView() causes blank bubblesDo return a valid View from createView(). If you return null, the message bubble renders as empty with no visible content.
Custom view data not updating when scrollingBind all dynamic data in bindView(), not in createView(). The bindView() method is called every time a ViewHolder is bound, while createView() is called only once per ViewHolder creation.
Template customization applies to wrong message typeVerify the template.getType() (Java) or template.type (Kotlin) matches the intended UIKitConstants.MessageType before applying customizations.
New custom template messages do not appear in the MessageListAdd the new CometChatMessageTemplate to the templates list using messageTemplates.add(contactTemplate) before calling messageList.setTemplates(messageTemplates).
Receipt icon appears on received messagesIn bindView(), check the alignment: if messageBubbleAlignment equals UIKitConstants.MessageBubbleAlignment.RIGHT, show the receipt; if LEFT, hide it with receipt.setVisibility(View.GONE).
Custom message type not matched by templateSet both setType() and setCategory() on the new template to match the custom message’s type and category (e.g., "card" and UIKitConstants.MessageCategory.CUSTOM).

FAQ

Q: Do I need to call setTemplates() every time I modify a template? A: Yes. After modifying any template in the templates list, call messageList.setTemplates(messageTemplates) to apply the changes to the MessageList. Without this call, the MessageList continues using the original templates. Q: What is the difference between setBubbleView() and setContentView()? A: setContentView() replaces only the content area of the message bubble while keeping the default header and footer views. setBubbleView() replaces the entire bubble — header, content, and footer — with a single custom layout. Use setBubbleView() when you need full control over the bubble appearance. Q: How do I create a template for a custom message type? A: Create a new CometChatMessageTemplate instance, call setType() with your custom type string (e.g., "card"), call setCategory() with UIKitConstants.MessageCategory.CUSTOM, define the bubble view using setBubbleView(), add the template to the templates list, and call messageList.setTemplates(). Q: Can I customize templates for built-in message types like text or image? A: Yes. Fetch the existing templates using CometChatUIKit.getDataSource().getAllMessageTemplates(), iterate through them to find the template matching the desired UIKitConstants.MessageType (e.g., TEXT or IMAGE), apply your customizations, and call messageList.setTemplates(). Q: What happens if createView() returns a view but bindView() does not set any data? A: The custom layout inflated in createView() is displayed with its default XML values (e.g., placeholder text, default visibility). Dynamic data such as the message text, sender name, or timestamp will not appear unless you set them in bindView().

Next steps

  • Message List component — Learn how to configure and customize the MessageList where templates are applied
  • Message Bubble styling — Explore styling options for the default message bubble views (Text Bubble, Image Bubble, File Bubble, Audio Bubble, Video Bubble)