Building Custom Message List Items With Compose

Creating custom message list items is a common requirement for many developers using Stream’s new Compose Chat SDK. In this post, you’ll learn just how easy it is to build your own custom message list items and implement them in your app.

Márton B.
Márton B.
Published December 8, 2021
Custom message list items feature image

The Stream Chat SDK for Jetpack Compose makes extensive use of slot APIs, which allow you to provide pieces of Composable layout that you can then use within one of the chat components provided by the SDK.

For this post, you'll use the MessageList component and its itemContentparameter to completely change how message items are rendered within a message list while keeping all the powerful built-in behavior, like paging and scrolling.

By the end, you'll build the following three custom looks for the items:

  • Colorful message bubbles
  • Livestream messages
  • A team messaging app
Message list items customizations

The itemContent parameter

A prime example of Stream's slot APIs is the MessageList component, which displays a list of messages:

Message list component

Within this component, the itemContent composable parameter is what renders each message in the list:

kotlin
1
2
3
4
5
6
7
MessageList( modifier = Modifier.fillMaxSize(), viewModel = listViewModel, itemContent = { item -> CustomMessageItemContent(item) // <- Your custom composable function }, )

This is what you'll customize in various ways, by providing our own itemContent components. You can try all of these implementations in the GitHub repository for this tutorial.

Note: We'll only cover the visual customization of the items here and skip attaching things like click listeners within the custom items, which is something you'd want to add in your real code.

Building Colorful Message Bubbles

Starting off with a look that's similar to the default, we'll build this design:

Customized message list bubbles

In this example, we're removing elements like the footer (which provides information like the user's name and timestamps) and reusing the Chat SDK's avatar and bubble components.

And of course, we're assigning a random color to each user's messages.

Let's see the code, with the important bits explained in the comments:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// Helper function to display a user avatar or a spacer @Composable private fun RowScope.MessageAvatar( position: MessageItemGroupPosition, user: User, ) { if (position == Bottom || position == None) { val isCurrentUser = user.id == ChatClient.instance().getCurrentUser()?.id // Provided by the SDK to display a User object's avatar UserAvatar( modifier = Modifier .padding(start = 8.dp, end = 8.dp) .size(24.dp) .clip(CircleShape) .background(Color.LightGray) .align(Alignment.Bottom), user = user, showOnlineIndicator = !isCurrentUser, ) } else { Spacer(modifier = Modifier.width(40.dp)) } } @Composable fun ColorfulChatItemContent(messageItem: MessageItem) { val (message, position) = messageItem val ownsMessage = messageItem.isMine Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), // Align messages left/right within the screen contentAlignment = if (ownsMessage) Alignment.CenterEnd else Alignment.CenterStart ) { Row( Modifier.widthIn(max = 300.dp) ) { // If the message is not by the current user, show avatar at the start if (!ownsMessage) { MessageAvatar(position, message.user) } Column( horizontalAlignment = if (ownsMessage) Alignment.End else Alignment.Start ) { val bubbleShape = RoundedCornerShape(16.dp).let { shape -> when (position) { Top, Middle -> shape // Round corners based on whose message it is else -> when (ownsMessage) { true -> shape.copy(bottomEnd = CornerSize(0)) false -> shape.copy(bottomStart = CornerSize(0)) } } } // Pick a random color from this hardcoded palette based on // the hash of the user ID (for consistency for each user) val colors = arrayOf( 0xFFFFADAD, 0xFFFFD6A5, 0xFFFDFFB6, 0xFFCAFFBF, 0xFF9BF6FF, 0xFFA0C4FF, 0xFFBDB2FF, 0xFFFFC6FF, ).map(::Color) val bubbleColor = colors[abs(message.user.id.hashCode()) % colors.size] // This component is provided by the SDK MessageBubble( modifier = Modifier.widthIn(max = 250.dp), shape = bubbleShape, color = bubbleColor, border = null, content = { Column { // Also built in to the SDK, so we can easily reuse it // to show content such as image attachments MessageAttachmentsContent(message = messageItem.message) if (message.text.isNotEmpty()) { Text( modifier = Modifier.padding( horizontal = 12.dp, vertical = 8.dp ), text = message.text, style = ChatTheme.typography.bodyBold ) } } } ) val spacerSize = if (position == None || position == Bottom) 4.dp else 2.dp Spacer(Modifier.size(spacerSize)) } // If the message is by the current user, show avatar at the end if (ownsMessage) { MessageAvatar(position, message.user) } } } }

As you can see, lots of this UI is built from basic Compose constructs, like Rows, Columns, Spacers, and Text.

However, smaller components from the SDK are also reused so that you don't have to construct everything from scratch. Components like UserAvatar or MessageAttachmentsContent really come in handy when you build custom layouts like these.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Building Livestream Messages

Our next design will be very basic, something you might add to a live-streaming screen within an application:

Customized livestream chat

This example won't support any attachments or fancy features. Instead, we will focus on displaying the user's messages in a compact UI.

This approach is ideal for handling real-time events where messages accumulate quickly.

Now, to the code:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Composable fun LiveStreamItemContent(messageItem: MessageItem) { val message = messageItem.message Column( modifier = Modifier .fillMaxWidth() .padding(vertical = 2.dp, horizontal = 4.dp) .clip(RoundedCornerShape(0.dp)) // Semi-transparent background for the live chat .background(Color(0x443474EB)) .padding(4.dp) .widthIn(max = 300.dp) ) { Row { UserAvatar( modifier = Modifier .padding(4.dp) .size(16.dp), user = message.user, showOnlineIndicator = false, ) // Combine the username and message into a single Text component Text( modifier = Modifier.padding(2.dp), text = buildAnnotatedString { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(message.user.name) } append(" ") append(message.text) }, style = ChatTheme.typography.bodyBold, fontSize = 14.sp ) } } }

There's not much to this one, as the design is super simple:

We reuse UserAvatar to build a string from the username and message.

This way, they can be styled and displayed within a single Text component, allowing them to flow nicely between lines.

Building Team Messaging Items

For the final example, let's look at something you might use for a team messaging app.

Customized team messaging items

As you can see, all the messages are left-aligned, and we prominently display the avatar and name of the user who sent the message.

This is useful in a corporate environment, where users won't recognize everyone just by their avatar (like they would in a small friend group).

The code for this custom layout is the following:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Composable fun TeamChatItemContent(messageItem: MessageItem) { val message = messageItem.message val firstItem = messageItem.groupPosition == Top || messageItem.groupPosition == None Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp) .widthIn(max = 300.dp) ) { if (firstItem) { Spacer(Modifier.padding(4.dp)) } Row { if (firstItem) { UserAvatar( modifier = Modifier .size(36.dp), shape = RoundedCornerShape(4.dp), user = message.user, ) } else { Spacer(modifier = Modifier.size(width = 36.dp, height = 0.dp)) } Column(modifier = Modifier.padding(horizontal = 8.dp)) { if (firstItem) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = message.user.name, style = TextStyle(fontWeight = FontWeight.Bold), fontSize = 14.sp ) Spacer(Modifier.size(4.dp)) // Another useful component from the SDK Timestamp(date = message.updatedAt ?: message.createdAt) } Spacer(Modifier.padding(2.dp)) } Text(text = message.text.trim()) Spacer(Modifier.padding(2.dp)) } } } }

Again, this layout builds mostly on basic Compose constructs, and most of the logic deals with setting correct paddings between the elements.

One highlight is the Timestamp component from the SDK, which you can use to quickly format a Date object into a date or time format.

Conclusion

As a quick reminder, you can check out all of the code and see these layouts in action by checking out the GitHub repository for this tutorial.

If you want to try the SDK and see how easy it is to get started, take a look at the Compose In-App Messaging Tutorial. You can also take a look at the Compose UI Components documentation which describes all the available components, features, and customization options.

You'll also find all the Compose SDK source code in the Stream Chat Android GitHub repository. We'd love to hear your feedback there, either in the form of issues or discussions.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->