Skip to main content
CometChatMessageList renders a scrollable list of messages for a conversation with real-time updates for new messages, edits, deletions, reactions, and threaded replies.

Where It Fits

CometChatMessageList is a message display component. It requires either a User or Group object to fetch and render messages. Wire it with CometChatMessageHeader and CometChatMessageComposer to build a complete messaging layout.
activity_chat.xml
<com.cometchat.uikit.kotlin.presentation.messagelist.ui.CometChatMessageList
    android:id="@+id/message_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
val messageList = findViewById<CometChatMessageList>(R.id.message_list)
messageList.setUser(user)

Quick Start

Add to your layout XML:
<com.cometchat.uikit.kotlin.presentation.messagelist.ui.CometChatMessageList
    android:id="@+id/message_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
Set a User or Group — this is required:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.your_layout)

    val messageList = findViewById<CometChatMessageList>(R.id.message_list)
    messageList.setUser(user)
    // or messageList.setGroup(group)
}
Prerequisites: CometChat SDK initialized with CometChatUIKit.init(), a user logged in, and the UI Kit dependency added.
Simply adding the MessageList component to the layout will only display the loading indicator. You must supply a User or Group object to fetch messages.

Filtering

Pass a MessagesRequest.MessagesRequestBuilder to control what loads:
messageList.setMessagesRequestBuilder(
    MessagesRequest.MessagesRequestBuilder()
        .setSearchKeyword("hello")
        .setLimit(30)
)

Filter Recipes

RecipeBuilder method
Search by keyword.setSearchKeyword("hello")
Filter by UID.setUID("user_uid")
Filter by GUID.setGUID("group_guid")
Limit per page.setLimit(30)
Unread only.setUnread(true)
Hide deleted messages.hideDeletedMessages(true)
Pass the builder object, not the result of .build(). The component calls .build() internally.

Actions and Events

Callback Methods

onThreadRepliesClick

Fires when a user taps a threaded message bubble.
messageList.setOnThreadRepliesClick { context, baseMessage, template ->
    // Navigate to thread view
}

onError

Fires on internal errors (network failure, auth issue, SDK exception).
messageList.setOnError { exception ->
    Log.e("MessageList", "Error: ${exception.message}")
}

onLoad

Fires when the list is successfully fetched and loaded.
messageList.setOnLoad { messages ->
    Log.d("MessageList", "Loaded ${messages.size}")
}

onEmpty

Fires when the list is empty after loading.
messageList.setOnEmpty {
    Log.d("MessageList", "No messages")
}

SDK Events (Real-Time, Automatic)

The component listens to SDK message events internally. No manual setup needed.
Automatic: new messages, edits, deletions, and reactions update the list in real time.

Functionality

Method (Kotlin XML)Compose ParameterDescription
setUser(user)user = userSet user for 1-on-1 conversation
setGroup(group)group = groupSet group for group conversation
setHideLoadingState(true)hideLoadingState = trueHide loading indicator
setHideEmptyState(true)hideEmptyState = trueHide empty state view
setHideErrorState(true)hideErrorState = trueHide error state view
setStartFromUnreadMessages(true)startFromUnreadMessages = trueScroll to first unread on load
setMessagesRequestBuilder(builder)messagesRequestBuilder = builderCustom message request builder
setBubbleFactories(list)bubbleFactories = listSet custom bubble factories for message types
setOnThreadRepliesClick { }onThreadRepliesClick = { }Thread reply tap callback

Custom View Slots

Header View

Custom view displayed at the top of the message list.
messageList.setHeaderView(View.inflate(context, R.layout.custom_header_layout, null))
Custom view displayed at the bottom of the message list.
messageList.setFooterView(View.inflate(context, R.layout.custom_footer_layout, null))

State Views

messageList.setEmptyView(customEmptyView)
messageList.setErrorView(customErrorView)
messageList.setLoadingView(customLoadingView)

Text Formatters (Mentions)

val mentionFormatter = CometChatMentionsFormatter(context)
mentionFormatter.setMessageListMentionTextStyle(context, R.style.CustomMentionsStyle)

val textFormatters: MutableList<CometChatTextFormatter> = ArrayList()
textFormatters.add(mentionFormatter)
messageList.setTextFormatters(textFormatters)

Bubble Factory

CometChatMessageList uses a factory-based pattern to render message content. Each message type (text, image, video, etc.) has a corresponding BubbleFactory that creates and binds the bubble view. You can replace existing factories or register new ones for custom message types.

How It Works

When a message is displayed, the list resolves a factory key from the message’s category and type (e.g., message_text, custom_location). If a matching BubbleFactory is registered, it handles view creation and binding. Otherwise, the built-in InternalContentRenderer handles default rendering. A BubbleFactory has two lifecycle phases:
  1. create*View(context) — called once when the ViewHolder is created. The message object is not available at this point.
  2. bind*View(view, message, alignment, ...) — called every time a message is displayed. This is where you populate the view with message data.

Replacing an Existing Bubble Factory

Override how a built-in message type renders by registering a factory with the same category and type:
class CustomTextBubbleFactory : BubbleFactory() {

    override fun getCategory(): String = CometChatConstants.CATEGORY_MESSAGE
    override fun getType(): String = CometChatConstants.MESSAGE_TYPE_TEXT

    override fun createContentView(context: Context): View {
        return TextView(context).apply {
            setPadding(24, 16, 24, 16)
        }
    }

    override fun bindContentView(
        view: View,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment,
        holder: RecyclerView.ViewHolder?,
        position: Int
    ) {
        (view as TextView).text = (message as? TextMessage)?.text ?: ""
    }
}

// Replace the default text bubble
messageList.setBubbleFactories(listOf(CustomTextBubbleFactory()))

Adding a New Bubble Factory for Custom Messages

Register a factory for a custom message type that the SDK doesn’t handle by default:
class LocationBubbleFactory : BubbleFactory() {

    override fun getCategory(): String = CometChatConstants.CATEGORY_CUSTOM
    override fun getType(): String = "location"

    override fun createContentView(context: Context): View {
        return LayoutInflater.from(context)
            .inflate(R.layout.bubble_location, null)
    }

    override fun bindContentView(
        view: View,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment,
        holder: RecyclerView.ViewHolder?,
        position: Int
    ) {
        val customMessage = message as? CustomMessage ?: return
        val lat = customMessage.customData?.optDouble("latitude") ?: 0.0
        val lng = customMessage.customData?.optDouble("longitude") ?: 0.0
        view.findViewById<TextView>(R.id.tv_coordinates).text = "$lat, $lng"
    }
}

messageList.setBubbleFactories(listOf(LocationBubbleFactory()))

Replacing the Entire Bubble

Override getBubbleView to replace the entire message bubble (including all slots like header, footer, avatar) instead of just the content area:
class FullCustomBubbleFactory : BubbleFactory() {

    override fun getCategory(): String = CometChatConstants.CATEGORY_CUSTOM
    override fun getType(): String = "meeting"

    override fun createBubbleView(context: Context): View {
        return LayoutInflater.from(context)
            .inflate(R.layout.bubble_meeting_full, null)
    }

    override fun bindBubbleView(
        view: View,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment,
        holder: RecyclerView.ViewHolder?,
        position: Int
    ) {
        val customMessage = message as? CustomMessage ?: return
        view.findViewById<TextView>(R.id.tv_meeting_title).text =
            customMessage.customData?.optString("title") ?: "Meeting"
    }
}

messageList.setBubbleFactories(listOf(FullCustomBubbleFactory()))

Bubble Slot Reference

Each BubbleFactory can override individual slots within the bubble:
SlotCreate MethodBind MethodDescription
Bubble (full)createBubbleView()bindBubbleView()Replaces the entire bubble. When non-null, all other slots are ignored.
ContentcreateContentView()bindContentView()Main message content (required).
LeadingcreateLeadingView()bindLeadingView()Avatar area on the left side.
HeadercreateHeaderView()bindHeaderView()Sender name and timestamp.
ReplycreateReplyView()bindReplyView()Quoted/reply-to message preview.
BottomcreateBottomView()bindBottomView()Below content (e.g., moderation).
Status InfocreateStatusInfoView()bindStatusInfoView()Timestamp and read receipts.
ThreadcreateThreadView()bindThreadView()Threaded replies indicator.
FootercreateFooterView()bindFooterView()Reactions and additional footer content.

Bubble Slot View Providers

While BubbleFactory customizes rendering per message type, slot view providers let you override a specific slot across all message types. This is useful when you want consistent customization (e.g., always show a custom avatar or always add a custom footer) regardless of the message type.
Slot view providers take priority over BubbleFactory slot methods. If both are set, the provider wins.
Use BubbleViewProvider — an interface with createView() (called once per ViewHolder) and bindView() (called each time a message binds):
// Custom leading view (avatar) for all messages
messageList.setLeadingViewProvider(object : BubbleViewProvider {
    override fun createView(
        context: Context,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment
    ): View? {
        return if (alignment == UIKitConstants.MessageBubbleAlignment.LEFT) {
            CometChatAvatar(context)
        } else null
    }

    override fun bindView(
        view: View,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment
    ) {
        (view as? CometChatAvatar)?.setUser(message.sender)
    }
})

// Custom status info view for all messages
messageList.setStatusInfoViewProvider(object : BubbleViewProvider {
    override fun createView(
        context: Context,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment
    ): View? {
        return TextView(context).apply { textSize = 10f }
    }

    override fun bindView(
        view: View,
        message: BaseMessage,
        alignment: UIKitConstants.MessageBubbleAlignment
    ) {
        (view as? TextView)?.text = formatTimestamp(message.sentAt)
    }
})
Available provider setters:
MethodSlot
setLeadingViewProvider()Avatar area
setHeaderViewProvider()Sender name / timestamp
setReplyViewProvider()Reply-to preview
setContentViewProvider()Main content (overrides all factories)
setBottomViewProvider()Below content (moderation)
setStatusInfoViewProvider()Timestamp / receipts
setThreadViewProvider()Thread replies indicator
setFooterViewProvider()Reactions / footer

Message Options

Message options are the contextual actions shown when a user long-presses a message bubble (e.g., Reply, Copy, Edit, Delete). You can control their visibility, replace the entire options list, or append custom options.

Toggling Default Option Visibility

Each built-in option has a visibility setter. Pass View.VISIBLE or View.GONE:
// Hide specific options
messageList.setDeleteMessageOptionVisibility(View.GONE)
messageList.setEditMessageOptionVisibility(View.GONE)
messageList.setTranslateMessageOptionVisibility(View.GONE)

// Show an option that's hidden by default
messageList.setMarkAsUnreadOptionVisibility(View.VISIBLE)
Available visibility methods (Kotlin XML):
MethodDefaultDescription
setReplyInThreadOptionVisibility()VISIBLEReply in thread
setReplyOptionVisibility()VISIBLEReply to message
setCopyMessageOptionVisibility()VISIBLECopy message text
setEditMessageOptionVisibility()VISIBLEEdit sent message
setDeleteMessageOptionVisibility()VISIBLEDelete message
setMessageReactionOptionVisibility()VISIBLEAdd reaction
setMessageInfoOptionVisibility()VISIBLEView message info
setTranslateMessageOptionVisibility()VISIBLETranslate message
setShareMessageOptionVisibility()VISIBLEShare message
setMarkAsUnreadOptionVisibility()GONEMark as unread

Replacing All Options (setOptions)

Use setOptions to completely replace the default options for a message. Return a list to override, or null to fall back to defaults:
messageList.setOptions { message ->
    listOf(
        CometChatMessageOption(
            id = "custom_pin",
            title = "Pin Message",
            icon = R.drawable.ic_pin,
            onClick = { pinMessage(message) }
        ),
        CometChatMessageOption(
            id = UIKitConstants.MessageOption.COPY,
            title = context.getString(R.string.cometchat_copy),
            icon = R.drawable.cometchat_ic_copy,
            onClick = { copyMessage(message) }
        )
    )
}
When setOptions returns a non-null list, addOptions is not invoked.

Appending Custom Options (addOptions)

Use addOptions to append additional options after the default ones. This is invoked only when setOptions is not set or returns null:
messageList.addOptions { message ->
    listOf(
        CometChatMessageOption(
            id = "custom_bookmark",
            title = "Bookmark",
            icon = R.drawable.ic_bookmark,
            onClick = { bookmarkMessage(message) }
        ),
        CometChatMessageOption(
            id = "custom_remind",
            title = "Remind Me",
            icon = R.drawable.ic_alarm,
            onClick = { setReminder(message) }
        )
    )
}

Conditional Options Per Message

Both setOptions and addOptions receive the BaseMessage, so you can return different options based on message type, sender, or any other condition:
messageList.setOptions { message ->
    when {
        // Custom options for your own messages
        message.sender?.uid == CometChat.getLoggedInUser()?.uid -> listOf(
            CometChatMessageOption(id = "edit", title = "Edit", icon = R.drawable.cometchat_ic_edit),
            CometChatMessageOption(id = "delete", title = "Delete", icon = R.drawable.cometchat_ic_delete)
        )
        // Default options for others' messages
        else -> null
    }
}

CometChatMessageOption Reference

PropertyTypeDescription
idStringUnique identifier (use UIKitConstants.MessageOption.* for built-in IDs).
titleStringDisplay text for the option.
titleColor@ColorInt IntText color (0 for default).
icon@DrawableRes IntDrawable resource for the option icon.
iconTintColor@ColorInt IntIcon tint color (0 for default).
titleAppearance@StyleRes IntText appearance style resource.
backgroundColor@ColorInt IntBackground color for the option row.
onClick(() -> Unit)?Callback invoked when the option is tapped.

Style

Define a custom style in themes.xml:
themes.xml
<style name="CustomMessageListStyle" parent="CometChatMessageListStyle">
    <item name="cometchatMessageListBackgroundColor">#F5F5F5</item>
</style>

<style name="AppTheme" parent="CometChatTheme.DayNight">
    <item name="cometchatMessageListStyle">@style/CustomMessageListStyle</item>
</style>
See Component Styling for the full reference.

ViewModel

val viewModel = ViewModelProvider(this)
    .get(CometChatMessageListViewModel::class.java)
messageList.setViewModel(viewModel)
See ViewModel & Data for state observation and custom repositories.

Next Steps

Message Header

Display user/group info in the toolbar

Message Composer

Rich input for sending messages

Message Template

Customize message bubble structure

Component Styling

Detailed styling reference