Custom Header for Message List

The message list header sits above the list of messages and shows the channel name and avatar, members and online members count, current connection status, and a back button. It also allows customization through its parameters.

In this example we will customize the MessageListHeader component and make it look similar to the WhatsApp conversation title bar.

Custom Message List Header

State Handling

Let’s define a new composable, CustomMessageListHeader, that takes the channel cid and a back handler as parameters.

@Composable fun CustomMessageListHeader(cid: String?, onBackClick: () -> Unit = {})

Inside it we’ll use the built-in MessageListViewModel to acquire the state that we need.

val viewModel = viewModel(  modelClass = MessageListViewModel::class.java,  factory = MessagesViewModelFactory(LocalContext.current, channelId = cid) )  val channel = viewModel.channel val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val currentUser by viewModel.user.collectAsStateWithLifecycle()

We pass channel, connectionState and currentUser to the MessageListHeader component, alongside other state that we get from the view model:

MessageListHeader(  channel = channel,  currentUser = currentUser,  connectionState = connectionState,  typingUsers = viewModel.typingUsers,  messageMode = viewModel.messageMode,  onBackPressed = onBackClick, )

Leading Content

Let’s customize the leading content, which represents the start slot of the header. This is a very simple customization: we just replace the default back arrow with our custom one.

Note that we’ll also use CustomHeaderButton for other buttons that we’ll add later to the header.

MessageListHeader(  // State  leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) }, )  @Composable private fun CustomHeaderLeadingContent(onClick: () -> Unit) {  CustomHeaderButton(  iconRes = R.drawable.ic_back,  contentDescription = "Back",  onClick = onClick  ) }  @Composable private fun CustomHeaderButton(  @DrawableRes iconRes: Int,  contentDescription: String,  onClick: () -> Unit, ) {  IconButton(  onClick = onClick,  content = {  Icon(  painter = painterResource(id = iconRes),  contentDescription = contentDescription,  tint = Color.White,  )  }  ) }

Center Content

Now, let’s use the middle slot of the header, named centerContent, and pass our CustomHeaderCenterContent to it.

We use several composables and utility methods in order to display the channel avatar, name, member and online member count:

  • The ChannelAvatar component from our SDK to show the avatar. Based on the state of the channel and the number of members, it shows different types of images.
  • The ChatTheme.ChannelNameFormatter.formatChannelName method to show the name of the channel, based on a set of rules. Search for the DefaultChannelNameFormatter component for more info.
  • The channel.getMembersStatusText extension method to show either a member count for a group channel or the last seen text for a direct one-to-one conversation.
MessageListHeader(  // State  leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },  centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) }, )  @Composable private fun CustomHeaderCenterContent(channel: Channel, currentUser: User?) {  Row {  ChannelAvatar(  modifier = Modifier.size(40.dp),  channel = channel,  currentUser = currentUser,  )  Spacer(modifier = Modifier.size(10.dp))  Column {  Text(  text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser),  color = Color.White,  fontWeight = FontWeight.SemiBold,  maxLines = 1,  overflow = TextOverflow.Ellipsis,  )  Text(  text = channel.getMembersStatusText(LocalContext.current, currentUser),  color = Color.LightGray,  style = ChatTheme.typography.footnote  )  }  } }

Trailing Content

Next, we’ll use the last slot, trailingContent, to add video and audio call buttons and a menu button, like WhatsApp has. As this is only an example, these buttons don’t do anything. You can check out our Video SDK to implement audio & video calling.

MessageListHeader(  // State  leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },  centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) },  trailingContent = { CustomHeaderTrailingContent() }, )  @Composable private fun CustomHeaderTrailingContent() {  Row(  modifier = Modifier  .fillMaxWidth()  .offset(x = 10.dp),  horizontalArrangement = Arrangement.End  ) {  CustomHeaderButton(  iconRes = R.drawable.ic_videocam,  contentDescription = "Video Call",  onClick = {}  )  CustomHeaderButton(  iconRes = R.drawable.ic_phone,  contentDescription = "Audio Call",  onClick = {}  )  CustomHeaderButton(  iconRes = R.drawable.ic_menu,  contentDescription = "Menu",  onClick = {}  )  } }

Final Touches

As final touches, we change the height and the background color of the header:

MessageListHeader(  // State  modifier = Modifier.height(55.dp),  color = Color(0xFF0F7B6F),  // Content slots )

Make sure to use ChatTheme as the root of all the composables that use our Compose UI Components. It’s used to provide default style properties and utility methods.

Full code

Below you can find the full code for our implementation.

@Composable fun CustomMessageListHeader(cid: String?, onBackClick: () -> Unit = {}) {  cid?.let {  val viewModel = viewModel(  modelClass = MessageListViewModel::class.java,  factory = MessagesViewModelFactory(LocalContext.current, channelId = cid)  )   val channel = viewModel.channel  val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()  val currentUser by viewModel.user.collectAsStateWithLifecycle()   MessageListHeader(  channel = channel,  currentUser = currentUser,  connectionState = connectionState,  modifier = Modifier.height(55.dp),  typingUsers = viewModel.typingUsers,  messageMode = viewModel.messageMode,  onBackPressed = onBackClick,  color = Color(0xFF0F7B6F),  leadingContent = { CustomHeaderLeadingContent(onClick = onBackClick) },  centerContent = { CustomHeaderCenterContent(channel = channel, currentUser = currentUser) },  trailingContent = { CustomHeaderTrailingContent() },  )  } }  @Composable private fun CustomHeaderLeadingContent(onClick: () -> Unit) {  CustomHeaderButton(  iconRes = R.drawable.ic_back,  contentDescription = "Back",  onClick = onClick  ) }  @Composable private fun CustomHeaderCenterContent(channel: Channel, currentUser: User?) {  Row {  ChannelAvatar(  modifier = Modifier.size(40.dp),  channel = channel,  currentUser = currentUser,  )  Spacer(modifier = Modifier.size(10.dp))  Column {  Text(  text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser),  color = Color.White,  fontWeight = FontWeight.SemiBold,  maxLines = 1,  overflow = TextOverflow.Ellipsis,  )  Text(  text = channel.getMembersStatusText(LocalContext.current, currentUser),  color = Color.LightGray,  style = ChatTheme.typography.footnote  )  }  } }  @Composable private fun CustomHeaderTrailingContent() {  Row(  modifier = Modifier  .fillMaxWidth()  .offset(x = 10.dp),  horizontalArrangement = Arrangement.End  ) {  CustomHeaderButton(  iconRes = R.drawable.ic_videocam,  contentDescription = "Video Call",  onClick = {}  )  CustomHeaderButton(  iconRes = R.drawable.ic_phone,  contentDescription = "Audio Call",  onClick = {}  )  CustomHeaderButton(  iconRes = R.drawable.ic_menu,  contentDescription = "Menu",  onClick = {}  )  } }  @Composable private fun CustomHeaderButton(@DrawableRes iconRes: Int, contentDescription: String, onClick: () -> Unit) {  IconButton(  onClick = onClick,  content = {  Icon(  painter = painterResource(id = iconRes),  contentDescription = contentDescription,  tint = Color.White,  )  }  ) }
© Getstream.io, Inc. All Rights Reserved.