#
Message ListMessageListView
is one of our core UI components, which displays a list of messages for a channel. It contains the following list of possible items:
- Plain text message
- Text and attachments (media or file) message
- Deleted message (only for current user)
- Error message (e.g. autoblocked message with inappropriate content)
- System message (e.g. some user joined a channel)
- Giphy preview
- Date separator
- Loading more indicator
- Thread separator (for thread mode only)
- Typing indicator
You're able to customize the appearance of this component using custom attributes as well as method calls at runtime. MessageListView
also contains the set of overridable action/option handlers and event listeners. By default, this component has the following look:
Light Mode | Dark Mode |
---|---|
![]() | ![]() |
#
UsageIf you want to use all default features and default design of this component then getting started with it is easy:
- Add the component to your xml layout hierarchy.
- Bind it with a
MessageListViewModel
.
Adding MessageListView
to your layout is easy as inserting following lines to your layout hierarchy (example for ConstraintLayout
):
<io.getstream.chat.android.ui.message.list.MessageListView
android:id="@+id/message_list_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
The UI components library includes a ViewModel for MessageListView
and the bindView
extension function makes it easy to use a default setup:
- Kotlin
- Java
// 1. Init view model
val viewModel: MessageListViewModel by viewModels {
MessageListViewModelFactory(cid = "messaging:123")
}
// 2. Bind view and viewModel
viewModel.bindView(messageListView, lifecycleOwner)
// Init view model
ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder()
.cid("messaging:123")
.build();
ViewModelProvider provider = new ViewModelProvider(this, factory);
MessageListViewModel viewModel = provider.get(MessageListViewModel.class);
// Bind view and viewModel
boolean enforceUniqueReactions = true;
MessageListViewModelBinding.bind(viewModel, messageListView, getViewLifecycleOwner(), enforceUniqueReactions);
#
Handling ActionsMessageListView
comes with a set of actions available out-of-the-box by long pressing a message:
- Adding reactions
- Replies
- Thread replies
- Copying the message
- Editing the message (if you are an owner)
- Deleting the message (if you are an owner)
- Flagging the message (if it doesn't belong to you)
Light Mode | Dark Mode |
---|---|
![]() | ![]() |
Default action handlers are set up when binding the ViewModel with the View. You can customize the default behavior by overriding each of the following handlers:
- Kotlin
- Java
messageListView.setLastMessageReadHandler {
// Handle when last message got read
}
messageListView.setEndRegionReachedHandler {
// Handle when end region reached
}
messageListView.setMessageDeleteHandler { message: Message ->
// Handle when message is going to be deleted
}
messageListView.setThreadStartHandler { message: Message ->
// Handle when new thread for message is started
}
messageListView.setMessageFlagHandler { message: Message ->
// Handle when message is going to be flagged
}
messageListView.setMessagePinHandler { message: Message ->
// Handle when message is going to be pinned
}
messageListView.setMessageUnpinHandler { message: Message ->
// Handle when message is going to be unpinned
}
messageListView.setGiphySendHandler { message: Message, giphyAction: GiphyAction ->
// Handle when some giphyAction is going to be performed
}
messageListView.setMessageRetryHandler { message: Message ->
// Handle when some failed message is going to be retried
}
messageListView.setMessageReactionHandler { message: Message, reactionType: String ->
// Handle when some reaction for message is going to be send
}
messageListView.setMessageReplyHandler { cid: String, message: Message ->
// Handle when message is going to be replied in the channel with cid
}
messageListView.setAttachmentDownloadHandler {
// Handle when attachment is going to be downloaded
}
messageListView.setLastMessageReadHandler(() -> {
// Handle when last message got read
});
messageListView.setEndRegionReachedHandler(() -> {
// Handle when end region reached
});
messageListView.setMessageDeleteHandler((message) -> {
// Handle when message is going to be deleted
});
messageListView.setThreadStartHandler((message) -> {
// Handle when new thread for message is started
});
messageListView.setMessageFlagHandler((message) -> {
// Handle when message is going to be flagged
});
messageListView.setMessagePinHandler((message) -> {
// Handle when message is going to be pinned
});
messageListView.setMessageUnpinHandler((message) -> {
// Handle when message is going to be unpinned
});
messageListView.setGiphySendHandler((message, giphyAction) -> {
// Handle when some giphyAction is going to be performed
});
messageListView.setMessageRetryHandler((message) -> {
// Handle when some failed message is going to be retried
});
messageListView.setMessageReactionHandler((message, reactionType) -> {
// Handle when some reaction for message is going to be send
});
messageListView.setMessageReplyHandler((cid, message) -> {
// Handle when message is going to be replied in the channel with cid
});
messageListView.setAttachmentDownloadHandler((attachmentDownloadCall) -> {
// Handle when attachment is going to be downloaded
});
messageListView.setMessageEditHandler((message) -> {
// Handle edit message
});
note
Handlers must be set before passing any data to MessageListView
. If you don't use the default binding that bindView
provides, please make sure you're setting up all handlers.
#
ListenersIn addition to the required handlers, MessageListView
also provides some optional listeners. They are also set by default if you use bindView
.
You can always override them to get events when something happens:
- Kotlin
- Java
messageListView.setMessageClickListener { message: Message ->
// Listen to click on message events
}
messageListView.setEnterThreadListener { message: Message ->
// Listen to events when enter thread associated with a message
}
messageListView.setAttachmentDownloadClickListener { attachment: Attachment ->
// Listen to events when download click for an attachment happens
}
messageListView.setUserReactionClickListener { message: Message, user: User, reaction: Reaction ->
// Listen to clicks on user reactions on the message options overlay
}
messageListView.setMessageLongClickListener { message ->
// Handle long click on message
}
messageListView.setAttachmentClickListener { message, attachment ->
// Handle long click on attachment
}
messageListView.setUserClickListener { user ->
// Handle click on user avatar
}
messageListView.setMessageClickListener((message) -> {
// Listen to click on message events
});
messageListView.setEnterThreadListener((message) -> {
// Listen to events when enter thread associated with a message
});
messageListView.setAttachmentDownloadClickListener((attachment) -> {
// Listen to events when download click for an attachment happens
});
messageListView.setUserReactionClickListener((message, user, reaction) -> {
// Listen to clicks on user reactions on the message options overlay
});
messageListView.setMessageLongClickListener((message) -> {
// Handle long click on message
});
messageListView.setAttachmentClickListener((message, attachment) -> {
// Handle long click on attachment
});
messageListView.setUserClickListener((user) -> {
// Handle click on user avatar
});
Other available listeners for MessageListView
can be found here.
#
CustomizationYou can change the appearance of this component to fit your app's design requirements. There are two ways to change the style: using XML attributes and runtime changes.
#
Using XML AttributesMessageListView
provides a large set of xml attributes available for customization. The full list of them is available here.
Let's consider an example when we want to change the style of messages sent by the current user.
Light Mode | Dark Mode |
---|---|
![]() | ![]() |
In order to do that, we need to add additional attributes to MessageListView
:
<io.getstream.chat.android.ui.message.list.MessageListView
android:id="@+id/messageListView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toTopOf="@+id/messageInputView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messagesHeaderView"
app:streamUiMessageBackgroundColorMine="#70AF74"
app:streamUiMessageBackgroundColorTheirs="#FFFFFF"
app:streamUiMessageTextColorMine="#FFFFFF"
app:streamUiMessageTextColorTheirs="#000000"
/>
#
Using Style TransformationsBoth MessageListView
and its ViewHolders can be configured programmatically (a list of supported customizations can be found here and here).
As an example, let's apply the green style from the previous section, but this time programmatically:
Before | After |
---|---|
![]() | ![]() |
We are going to use a custom TransformStyle.messageListItemStyleTransformer
:
- Kotlin
- Java
TransformStyle.messageListItemStyleTransformer = StyleTransformer { defaultViewStyle ->
defaultViewStyle.copy(
messageBackgroundColorMine = Color.parseColor("#70AF74"),
messageBackgroundColorTheirs = Color.WHITE,
textStyleMine = defaultViewStyle.textStyleMine.copy(color = Color.WHITE),
textStyleTheirs = defaultViewStyle.textStyleTheirs.copy(color = Color.BLACK),
)
}
TransformStyle.setMessageListItemStyleTransformer(source -> {
// Customize the theme
return source;
});
note
The transformers should be set before the views are rendered to make sure that the new style was applied.
As another example, let's modify the default view which allows scrolling to the bottom when the new message arrives:
Before | After |
---|---|
![]() | ![]() |
To achieve this effect we need to provide this custom TransformStyle.messageListStyleTransformer
:
- Kotlin
- Java
TransformStyle.messageListStyleTransformer = StyleTransformer { defaultViewStyle ->
defaultViewStyle.copy(
scrollButtonViewStyle = defaultViewStyle.scrollButtonViewStyle.copy(
scrollButtonColor = Color.RED,
scrollButtonUnreadEnabled = false,
scrollButtonIcon = ContextCompat.getDrawable(requireContext(), R.drawable.stream_ui_ic_clock)!!,
),
)
}
TransformStyle.setMessageListStyleTransformer(source -> {
// Customize the theme
return source;
});
#
Channel Feature FlagsSome xml attributes let you to enable/disable features in MessageListView
.
streamUiScrollButtonEnabled
- show/hide the scroll-to-bottom buttonstreamUiScrollButtonUnreadEnabled
- show/hide the unread count badge on the scroll-to-bottom buttonstreamUiReactionsEnabled
- whether users can react to messagesstreamUiReplyEnabled
- whether users can reply to messagesstreamUiCopyMessageActionEnabled
- whether users can copy messagesstreamUiRetryMessageEnabled
- whether users can retry failed messagesstreamUiEditMessageEnabled
- whether users can edit their messagesstreamUiFlagMessageEnabled
- whether users can flag messagesstreamUiFlagMessageConfirmationEnabled
- whether users will see the confirmation dialog when flag messagesstreamUiDeleteMessageEnabled
- whether users can delete their messagesstreamUiDeleteConfirmationEnabled
- whether users will see the confirmation dialog when deleting messagesstreamUiThreadsEnabled
- whether users can create thread replies
These attributes let you enable/disable configuration for channel features. E.g. if a channel's configuration supports message replies, but you disabled it via xml attributes, then members of this channel won't see such an option.
MessageListView
provides you the possibility to enable/disable these channel features at runtime as well:
- Kotlin
- Java
messageListView.setRepliesEnabled(false)
messageListView.setDeleteMessageEnabled(false)
messageListView.setEditMessageEnabled(false)
messageListView.setRepliesEnabled(false);
messageListView.setDeleteMessageEnabled(false);
messageListView.setEditMessageEnabled(false);
Before | After |
---|---|
![]() | ![]() |
#
Messages Start PositionYou can configure the messages to start at the top or the bottom of the view (Default: bottom) by using streamUiMessagesStart
and streamUiThreadMessagesStart
attributes.
Bottom | Top |
---|---|
![]() | ![]() |
note
Messages' position doesn't affect the stack. The default is from bottom to top. if you would like to change it, use the method setCustomLinearLayoutManager
and set a LinearLayoutManager
with your desired definitions.
#
Filtering MessagesYou can filter some messages if you don't want to show them in your MessageListView
.
Imagine you want to hide all messages that contain the word "secret". This can be done with following lines:
- Kotlin
- Java
val forbiddenWord = "secret"
val predicate = MessageListView.MessageListItemPredicate { item ->
!(item is MessageListItem.MessageItem && item.message.text.contains(forbiddenWord))
}
messageListView.setMessageListItemPredicate(predicate)
String forbiddenWord = "secret";
messageListView.setMessageListItemPredicate(item -> {
if (item instanceof MessageListItem.MessageItem) {
MessageListItem.MessageItem messageItem = (MessageListItem.MessageItem) item;
return !((MessageListItem.MessageItem) item).getMessage().getText().contains(forbiddenWord);
}
return true;
});
note
The predicate has to return true
for the items that you do want to display in the list.
#
Custom Message ViewsMessageListView
provides an API for creating custom ViewHolders. To use your own ViewHolder:
- Extend
MessageListItemViewHolderFactory
. - Write your own logic for creating ViewHolders.
- Create a new factory instance and set it on
MessageListView
.
Let's consider an example when we want to create custom ViewHolders for messages that came from other users less than 24 hours ago. The result should look like this:
- Add a new layout called
today_message_list_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="40dp"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="@android:color/holo_green_dark"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/marginEnd"
app:layout_constraintTop_toTopOf="parent"
>
<TextView
android:id="@+id/textLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@android:color/primary_text_light"
android:padding="16dp"
/>
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/marginEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.7"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- Add a new
TodayViewHolder
class that inflates this layout and populates it with data:
- Kotlin
- Java
class TodayViewHolder(
parentView: ViewGroup,
private val binding: TodayMessageListItemBinding = TodayMessageListItemBinding.inflate(LayoutInflater.from(
parentView.context),
parentView,
false),
) : BaseMessageItemViewHolder<MessageListItem.MessageItem>(binding.root) {
override fun bindData(data: MessageListItem.MessageItem, diff: MessageListItemPayloadDiff?) {
binding.textLabel.text = data.message.text
}
}
class TodayViewHolder extends BaseMessageItemViewHolder<MessageListItem.MessageItem> {
TodayMessageListItemBinding binding;
public TodayViewHolder(@NonNull ViewGroup parentView, @NonNull TodayMessageListItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
@Override
public void bindData(@NonNull MessageListItem.MessageItem data, @Nullable MessageListItemPayloadDiff diff) {
binding.textLabel.setText(data.getMessage().getText());
}
}
- Add a new
CustomMessageViewHolderFactory
class that checks each message, and uses the custom ViewHolder when necessary:
- Kotlin
- Java
class CustomMessageViewHolderFactory : MessageListItemViewHolderFactory() {
override fun getItemViewType(item: MessageListItem): Int {
return if (item is MessageListItem.MessageItem &&
item.isTheirs &&
item.message.attachments.isEmpty() &&
item.message.createdAt.isLessThenDayAgo()
) {
TODAY_VIEW_HOLDER_TYPE
} else {
super.getItemViewType(item)
}
}
private fun Date?.isLessThenDayAgo(): Boolean {
if (this == null) {
return false
}
val dayInMillis = TimeUnit.DAYS.toMillis(1)
return time >= System.currentTimeMillis() - dayInMillis
}
override fun createViewHolder(
parentView: ViewGroup,
viewType: Int,
): BaseMessageItemViewHolder<out MessageListItem> {
return if (viewType == TODAY_VIEW_HOLDER_TYPE) {
TodayViewHolder(parentView)
} else {
super.createViewHolder(parentView, viewType)
}
}
companion object {
private const val TODAY_VIEW_HOLDER_TYPE = 1
}
}
class CustomMessageViewHolderFactory extends MessageListItemViewHolderFactory {
private int TODAY_VIEW_HOLDER_TYPE = 1;
@Override
public int getItemViewType(@NonNull MessageListItem item) {
if (item instanceof MessageListItem.MessageItem) {
MessageListItem.MessageItem messageItem = (MessageListItem.MessageItem) item;
if (messageItem.isTheirs()
&& messageItem.getMessage().getAttachments().isEmpty()
&& isLessThanDayAgo((messageItem.getMessage().getCreatedAt()))) {
return TODAY_VIEW_HOLDER_TYPE;
}
}
return super.getItemViewType(item);
}
private boolean isLessThanDayAgo(Date date) {
if (date == null) return false;
long dayInMillis = TimeUnit.DAYS.toMillis(1);
return date.getTime() >= System.currentTimeMillis() - dayInMillis;
}
@NonNull
@Override
public BaseMessageItemViewHolder<? extends MessageListItem> createViewHolder(@NonNull ViewGroup parentView, int viewType) {
if (viewType == TODAY_VIEW_HOLDER_TYPE) {
return new TodayViewHolder(parentView, TodayMessageListItemBinding.inflate(LayoutInflater.from(parentView.getContext()), parentView, false));
}
return super.createViewHolder(parentView, viewType);
}
}
- Finally, set an instance of the custom factory on
MessageListView
- Kotlin
- Java
messageListView.setMessageViewHolderFactory(CustomMessageViewHolderFactory())
messageListView.setMessageViewHolderFactory(new CustomMessageViewHolderFactory());
Additionally, you can also use ChatUI.markdown
for Markdown support. If you do that, don't use android:autoLink
attribute because it'll break the markdown Linkify implementation. This property is deprecated and ChatUI.messageTextTransformer
should be used instead. See docs for more information.
#
Custom Empty StateMessageListView
handles loading and empty states out-of-box. If you want to customize these, you can do it at runtime.
Let's consider an example when you want to set a custom empty state:
- Kotlin
- Java
val textView = TextView(context).apply {
text = "There are no messages yet"
setTextColor(Color.RED)
}
messageListView.setEmptyStateView(
view = textView,
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
)
TextView textView = new TextView(getContext());
textView.setText("There are no messages yet");
textView.setTextColor(Color.RED);
messageListView.setEmptyStateView(
textView,
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
);
This code will display the following empty state:
#
Configure When Avatar AppearsIs it possible to configure when the avatar for messages appears. You can use MessageListView.setShowAvatarPredicate
and pass a predicate to define when the avatar is going to be shown. This example implements the default behaviour:
- Kotlin
- Java
messageListView.setShowAvatarPredicate(
ShowAvatarPredicate { messageItem ->
messageItem.positions.contains(MessageListItem.Position.BOTTOM) && messageItem.isTheirs
}
)
messageListView.setShowAvatarPredicate((messageItem) -> messageItem.getPositions().contains(MessageListItem.Position.BOTTOM) && messageItem.isTheirs());
note
To avoid overlap between the avatar and the messages of the chat, remember to use streamUiMessageStartMargin
and streamUiMessageEndMargin
to create space for the avatar of the messages.
If you set a predicate that shows avatars for your own messages as well, use this value:
streamUiMessageEndMargin=">@dimen/stream_ui_message_viewholder_avatar_missing_margin"
If your predicate doesn't show avatars for your own messages (this is the default behavior), remove the end margin:
streamUiMessageEndMargin="0dp"
#
Configure giphyIt is possible to configure the gifs size. Prior of version 4.23.0 gifs were auto-sizable and will resize themselves accordingly with the size of the image they load.
As of v5.0.0, our Giphy holders are much more customizable. You can use three Giphy load modes:
original
: The original Giphy file size, as given by the API.fixedHeight
: Smaller file size Giphy, that uses fixed height in order to improve scrolling performance and image loading speed.fixedHeightDownsampled
: Same asfixedHeight
, but downsampled to only a few frames, making it roughly 6x smaller and faster to load, but less visually appealing.
Alongside these Giphy types, you can also choose the scaleType
when loading the Giphy:
fitCenter
: The recommended setting, fits the GIF to the center of the container and keeps the aspect ratio.centerCrop
: Useful if you want larger size giphies and items, but don't care if the image is cropped.center
: Doesn't scale the image and centers it in the parent. Not recommended for most Giphy images as it will cause a lot of whitespace and smaller images.- ...
We recommend always using fitCenter
as the go-to way of loading Giphy images that doesn't cause images to crop. For the best performance, we recommend either fixedHeight
, or fixedHeightDownsampled
if you don't mind the low frame rate.