# Message List

The `MessageList` component is a crucial part of building a chat experience. We support two versions of the `MessageList` component:

- **Bound**: This version binds itself to the `MessageListViewModel` and loads all the required data. It also connects single and long item tap, pagination, and bottom reached events to the `ViewModel`.
- **Stateless**: This is a stateless version of the list which doesn't know about the `ViewModel` and depends on pure state from external sources to render its UI.

<admonition type="note">

The **bound** version of the list uses the **stateless** list internally. That way, when providing the same state to either component, the behavior remains the same.

Additionally, we cannot provide a default `ViewModel` for this component, as it requires the `channelId` to load the data, so you'll have to build an instance yourself.

</admonition>

Based on the provided state, this component shows the following UI:

- **Loading indicator**: While we're loading the initial data.
- **Empty content**: If there is no data and we've finished loading.
- **Messages**: The list of messages in the channel, including file and image attachments, with various actions like thread clicks, item long taps, pagination and reaching the bottom.

Let's see how to show a list of messages.

## Usage

To use the **bound** `MessageList`, first initialize the `ViewModel` using our `MessagesViewModelFactory`:

```kotlin
val factory by lazy {
    MessagesViewModelFactory(
        context = this,
        channelId = channelId,
    )
}

val listViewModel by viewModels<MessageListViewModel>(factoryProducer = { factory })
```

Then add it to the rest of your UI, for example within `setContent()`:

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Load data

    setContent {
        ChatTheme {
            Column(
                Modifier.fillMaxSize()
            ) {
                MessageListHeader(...)

                MessageList(
                    viewModel = listViewModel,
                    modifier = Modifier.fillMaxSize()
                )

                // Rest of your UI
            }
        }
    }
}
```

As you can see, it's easy to add the component to your UI and combine it with our other components (or your own) to build a custom screen. Additionally, if you choose the **bound** version, as seen here, you just need to provide a `MessageListViewModel` and the component will work on its own.

The snippet above will produce the following UI.

![The Default MessageList component with a MessageListHeader](@chat-sdk/android/v6/_assets/compose_default_message_list_component.png)

Notice how easy it was to integrate this component with other composable functions, like our `MessageListHeader`. You can see that the component shows different types of messages, such as link and image previews. It also handles pagination and various other events when scrolling or receiving new messages.

Let's see how to handle the actions within the list.

## Handling Actions

The `MessageList` component exposes the following actions, as per the signature:

```kotlin
@Composable
fun MessageList(
    ..., // State & UI
    // Message actions
    onThreadClick: (Message) -> Unit = { viewModel.openMessageThread(it) },
    onLongItemClick: (Message) -> Unit = { viewModel.selectMessage(it) },
    onReactionsClick: (Message) -> Unit = { viewModel.selectReactions(it) },
    onGiphyActionClick: (GiphyAction) -> Unit = { viewModel.performGiphyAction(it) },
    onQuotedMessageClick: (Message) -> Unit = { viewModel.scrollToMessage(it.id) },
    onUserAvatarClick: ((User) -> Unit)? = null,
    onMessageLinkClick: ((Message, String) -> Unit)? = null,
    onUserMentionClick: (User) -> Unit = {},
    onReply: (Message) -> Unit = {},
    // Poll actions
    onPollUpdated: (Message, Poll) -> Unit = { message, poll -> ... },
    onCastVote: (Message, Poll, Option) -> Unit = { message, poll, option -> ... },
    onRemoveVote: (Message, Poll, Vote) -> Unit = { message, poll, vote -> ... },
    selectPoll: (Message, Poll, PollSelectionType) -> Unit = { message, poll, selectionType -> ... },
    onAddAnswer: (Message, Poll, String) -> Unit = { message, poll, answer -> ... },
    onClosePoll: (String) -> Unit = { pollId -> ... },
    onAddPollOption: (Poll, String) -> Unit = { poll, option -> ... },
    // Pagination and scroll actions
    onMessagesPageStartReached: () -> Unit = { viewModel.loadOlderMessages() },
    onMessagesPageEndReached: (String) -> Unit = { viewModel.loadNewerMessages(it) },
    onLastVisibleMessageChanged: (Message) -> Unit = { viewModel.updateLastSeenMessage(it) },
    onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() },
    onScrollToBottomClicked: (() -> Unit) -> Unit = { viewModel.scrollToBottom(scrollToBottom = it) },
    // Media actions
    onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = { ... },
    onPauseAudioRecordingAttachments: () -> Unit = {},
    ... // Content
)
```

### Message Actions

- `onThreadClick`: Handler used when the user taps on a message with a thread.
- `onLongItemClick`: Handler used when the user long taps on an item.
- `onReactionsClick`: Handler used when the user taps on a reaction to a message.
- `onGiphyActionClick`: Handler used when the user clicks on one of the actions in a Giphy message. Giphy images with actions are displayed only directly after using the Giphy slash command.
- `onQuotedMessageClick`: Handler used when the user clicks on a quoted message. By default will search for the message that has been quoted and will scroll to it.
- `onUserAvatarClick`: Handler used when the user taps on another user's avatar. If `null`, the avatar won't be clickable.
- `onMessageLinkClick`: Handler used when the user taps on a link within a message. Receives both the `Message` and the URL `String` that was clicked.
- `onUserMentionClick`: Handler used when the user taps on a user mention (e.g., `@username`) within a message.
- `onReply`: Handler used when the user triggers a reply action on a message.

### Poll Actions

- `onPollUpdated`: Handler used when a poll is updated.
- `onCastVote`: Handler used when the user casts a vote on a poll option.
- `onRemoveVote`: Handler used when the user removes their vote from a poll.
- `selectPoll`: Handler used when the user selects a poll to view more options, results, or answers.
- `onAddAnswer`: Handler used when the user adds an answer to a poll (for polls that allow user-suggested answers).
- `onClosePoll`: Handler used when the poll creator closes a poll.
- `onAddPollOption`: Handler used when the user adds a new option to a poll.

### Pagination and Scroll Actions

- `onMessagesPageStartReached`: Handler used when the user reaches the oldest loaded message, to trigger pagination.
- `onMessagesPageEndReached`: Handler used when the user reaches the newest loaded message, to trigger pagination.
- `onLastVisibleMessageChanged`: Handler used when the user scrolls and the last visible item changes.
- `onScrollToBottom`: Handler used when the user reaches the newest message. Used to remove the "New message" or "Scroll to bottom" actions from the UI.
- `onScrollToBottomClicked`: Handler used when the user clicks on the scroll to bottom button.

### Media Actions

- `onMediaGalleryPreviewResult`: Handler used when the user receives a result from the Media Gallery Preview screen, after opening an image or a video attachment.
- `onPauseAudioRecordingAttachments`: Handler used to pause any playing audio recording attachments when needed (e.g., when navigating away).

You can customize the behavior here by providing your own actions, like so:

```kotlin
MessageList(
    ..., // State
    onThreadClick = { message -> },
    onLongItemClick = { message -> },
    onReactionsClick = { message -> },
    onUserAvatarClick = { user ->
        // Navigate to user profile
    },
    onMessageLinkClick = { message, url ->
        // Handle link click
    },
    onUserMentionClick = { user ->
        // Navigate to mentioned user's profile
    },
    // ... other handlers
)
```

If you're using the **bound** version of the component, these actions update the state within the `ViewModel` by default, while the default actions of the **stateless** version are all empty.

If you override the default actions to build your custom behavior, we still recommend storing the data in the `ViewModel`, as most of the behavior like having threads and pagination is already built for you.

We recommend using the **bound** version for ease of use. Alternatively, you can use the stateless version and provide the data manually, for more control.

### Controlling the scroll state

The `MessageList` allows you to control the scroll state of the list by providing a `messagesLazyListState` parameter, like so:

```kotlin
@Composable
fun MessageList(
    ..., // State, UI & Actions
    messagesLazyListState: MessagesLazyListState = rememberMessageListState(parentMessageId = viewModel.currentMessagesState.parentMessageId),
    ... // Content
)
```

- `messagesLazyListState`: The scroll state of the list. While not a handler, you can use it to control the scroll and trigger custom scroll actions.

You can customize this state in the following way:

```kotlin
val myListState = rememberMessageListState(parentMessageId = state.parentMessageId)

MessageList(
    ..., // the rest of the state, UI & actions
    messagesLazyListState = myListState,
)
```

`rememberMessageListState()` keeps a separate instance of `MessagesLazyListState` based on the `parentMessageId`. This helps you keep the scroll state of the main list intact when you enter and leave threads.

You can also provide your own instance of `MessagesLazyListState` where you define the starting scroll position of the state and how the focused message offset is calculated. For example, clicking on the original message contained within the reply will automatically scroll to the message and put it in focus. You can define where on the screen the focused message appears by passing in your own `MessageOffsetHandler`.

## Previewing Attachments

Out of the box previews are provided for the following attachment types: uploading, link, Giphy, image, video and file.

### Image and Video

Image and video attachments are previewed as thumbnails which can be displayed as a single tile or multiple ones depending on how many attachments are contained within the specific message. By default, these are rendered by `MediaAttachmentFactory`.

<admonition type="note">

Attachment factories are used for previewing attachments. To learn more about factories and how to customize them, click on the link [here](/chat/docs/sdk/android/v6/compose/general-customization/chat-theme#streamattachmentfactories/).

</admonition>

In practice they appear as such:

![Default Image and Video Attachment Previews](@chat-sdk/android/v6/_assets/compose_message_list_stock_media_attachments.png)

Video thumbnails are enabled by default but can be turned off by setting the [ChatTheme](/chat/docs/sdk/android/v6/compose/general-customization/chat-theme/) property `videoThumbnailsEnabled` to `false`.

Once video thumbnails are disabled, messages containing video attachments will be displayed in the following manner:

![Video Thumbnails Disabled](@chat-sdk/android/v6/_assets/compose_message_list_stock_media_attachments_video_thumbnails_disabled.png)

If you want to read more about how to customize the way these attachments are displayed, head over to the relevant section seen [here](/chat/docs/sdk/android/v6/compose/guides/customizing-image-and-video-previews/)

## Customization

Both the **bound** and **stateless** versions of `MessageList` share the same customization parameters. The only difference is how you provide the data:

- **Bound version**: Pass a `viewModel: MessageListViewModel` that manages the state internally.
- **Stateless version**: Pass a `currentState: MessageListState` that you manage yourself.

```kotlin
@Composable
fun MessageList(
    // Bound version uses: viewModel: MessageListViewModel
    // Stateless version uses: currentState: MessageListState
    reactionSorting: ReactionSorting = ReactionSortingByFirstReactionAt,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
    messagesLazyListState: MessagesLazyListState = rememberMessageListState(...),
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    threadsVerticalArrangement: Arrangement.Vertical = Arrangement.Bottom,
    ..., // Actions
    loadingContent: @Composable () -> Unit = { /* Default loading indicator */ },
    emptyContent: @Composable () -> Unit = { /* Default empty content */ },
    helperContent: @Composable BoxScope.() -> Unit = { /* Default scroll helper */ },
    loadingMoreContent: @Composable () -> Unit = { /* Default loading more indicator */ },
    itemContent: @Composable (MessageListItemState) -> Unit = { /* Default message container */ },
)
```

- `reactionSorting`: Defines how reactions are sorted when displayed. Defaults to `ReactionSortingByFirstReactionAt`, which sorts reactions by the time the first reaction of each type was added.
- `modifier`: Modifier for the root component. Useful for things like the component size, padding, background and similar.
- `contentPadding`: Padding values to be applied to the message list surrounding the content inside.
- `messagesLazyListState`: The scroll state of the list. You can use it to control the scroll and trigger custom scroll actions.
- `verticalArrangement`: Controls the vertical arrangement of messages in the list. Defaults to `Arrangement.Top`.
- `threadsVerticalArrangement`: Controls the vertical arrangement of messages when viewing a thread. Defaults to `Arrangement.Bottom` so the most recent messages appear at the bottom.
- `loadingContent`: Used while loading the message data. By default shows a loading indicator that you can override.
- `emptyContent`: An empty placeholder used when no messages are available. By default displays a message that you can override.
- `helperContent`: Composable that represents helper content for the message list. By default handles the scrolling behavior and shows a floating button that can be used to scroll to the bottom.
- `loadingMoreContent`: Composable that represents the loading more content, when we're loading the next page.
- `itemContent`: Composable that allows you to fully override the UI and behavior of each message in the list. This function will be applied to each item in the list and you'll gain access to the `MessageListItemState` inside the lambda when building your custom UI.

Here's an example of customizing the `Message` items in the list (using the bound version):

```kotlin
MessageList(
    viewModel = listViewModel, // or `currentState = messageListState` for stateless
    modifier = Modifier
        .fillMaxSize()
        .background(ChatTheme.colors.appBackground),
    itemContent = { item ->
        if (item is MessageItemState) { // we check against other subclasses of 'MessageListItemState'
            val message = item.message

            Column(
                modifier = Modifier
                    .padding(8.dp)
                    .widthIn(max = 300.dp)
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Avatar(
                        modifier = Modifier.size(36.dp),
                        imageUrl =  message.user.image,
                        initials = message.user.initials
                    )

                    Text(
                        modifier = Modifier.padding(start = 8.dp),
                        text = message.user.name,
                        style = ChatTheme.typography.bodyBold,
                        fontSize = 14.sp,
                        color = ChatTheme.colors.textHighEmphasis
                    )
                }

                MessageBubble(
                    color = ChatTheme.colors.barsBackground,
                    modifier = Modifier.padding(top = 4.dp),
                    shape = RoundedCornerShape(
                        topEnd = 16.dp,
                        topStart = 0.dp,
                        bottomEnd = 16.dp,
                        bottomStart = 16.dp
                    ),
                    content = {
                        Text(
                            modifier = Modifier.padding(8.dp),
                            text = message.text,
                            color = ChatTheme.colors.textHighEmphasis
                        )
                    }
                )
            }
        }
    }
)
```

This snippet of code prepares a custom message item component, using the `itemContent` parameter. It features a `Column` that hosts a `Row` with an avatar and a text showing the user name, as well as a `MessageBubble` that wraps the message text.

These components also use modifiers and other properties to style them and make them look nicer. With a simple parameter, this snippet will now produce the following UI in the `MessageList`:

![Custom MessageList items](@chat-sdk/android/v6/_assets/compose_custom_message_list_component.png)

As per our description, the `Avatar` and the user name `Text` are shown in a `Row`, after which we see the `MessageBubble`. Note that this approach doesn't automatically display attachments, so you'll have to show attachment UI based on the provided `attachmentFactories` within the `ChatTheme`.

Using this approach, you can completely customize the items to your needs and you can use click, touch and combined modifiers to customize the touch event behavior.

And you can customize the `emptyContent` and the `loadingContent` to your needs, if you need custom UI there.


---

This page was last updated at 2026-04-17T17:33:31.438Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/v6/compose/message-components/message-list/](https://getstream.io/chat/docs/sdk/android/v6/compose/message-components/message-list/).