Skip to main content

Channel List#

ChannelListView is a component that displays a list of channels available to a user. For users with a slower connection or that don't belong to any channels yet, ChannelListView also supports loading and empty states.

The Channel List component

By default, a single list item shows the following:

  • Channel name
  • User's read state
  • Last message
  • Timestamp of the last message

It also implements swiping behaviour which allows handling different actions on the channel.


To use ChannelListView, include it in your XML layout as shown below:

<    android:id="@+id/channelsView"    android:layout_width="match_parent"    android:layout_height="match_parent" />

We recommend using the ChannelListViewModel to get the full list of data from the Stream API and then render it using the ChannelListView.

The basic setup of the ViewModel and connecting it to the View is done the following way:

// Instantiate the ViewModelval viewModel: ChannelListViewModel by viewModels {    ChannelListViewModelFactory(        filter = Filters.and(            Filters.eq("type", "messaging"),            Filters.`in`("members", listOf(ChatDomain.instance().user.value!!.id)),         ),        sort = ChannelListViewModel.DEFAULT_SORT,        limit = 30,    )}// Bind the ViewModel with ChannelListViewviewModel.bindView(channelListView, viewLifecycleOwner)

To instantiate ChannelListViewModel you may want to pass an instance of the ChatEventHandler interface. See this point to read more.

All the logic of subscribing to data emitted by the ViewModel is provided by the bindView function. Other than channel data loading, the ViewModel is also handling actions like deleting a channel and leaving a group conversation by default.

Handling Actions#

ChannelListView includes a set of channel actions. Actions on ChannelListView items are available on swipe. With these, you can:

  • Delete the channel if you have sufficient permissions
  • See channel members
  • Leave the channel if it's a group channel
Light ModeDark Mode

The following actions are not implemented by default, so you should add your own listeners if you want to handle them:

channelListView.setChannelItemClickListener { channel ->    // Handle channel click}channelListView.setChannelInfoClickListener { channel ->    // Handle channel info click}channelListView.setUserClickListener { user ->    // Handle member click}

The full list of available listeners is available here.


There are two ways to customize the appearance of ChannelListView:

  • Using XML attributes
  • Using the TransformStyle API at runtime to customize the style of all ChannelListView instances

Using XML Attributes#

There are many XML attributes that can be used to customize the appearance of the channel list. The most useful ones include:

  • streamUiChannelDeleteEnabled: Whether the delete icon should be displayed.
  • streamUiChannelDeleteIcon: Drawable reference for the channel delete icon.
  • streamUiChannelTitleTextColor: Color of the channel title text.
  • streamUiChannelTitleTextSize: Size of the channel title text.
  • streamUiLastMessageTextSize: Size of the last message text.
  • streamUiLastMessageTextColor: Color of the last message text.
  • streamUiForegroundLayoutColor: Foreground color of the channel list item.
  • streamUiBackgroundLayoutColor: Background color of the channel list item, visible when swiping the list item.

The full list of available XML attributes is available under ChannelListView styleable, here.

Using Style Transformations#

The following example shows how to modify the style of all ChannelListView instances globally to:

  • Disable the default options
  • Change the title text style
  • Make the default icons larger
  • Change the background color for unread messages

To make these changes, we need to define a custom TransformStyle.channelListStyleTransformer:

TransformStyle.channelListStyleTransformer = StyleTransformer { defaultStyle ->    defaultStyle.copy(        optionsEnabled = false,        foregroundLayoutColor = Color.LTGRAY,        indicatorReadIcon = ContextCompat.getDrawable(requireContext(), R.drawable.stream_ui_ic_clock)!!,        channelTitleText = TextStyle(            color = Color.WHITE,            size = resources.getDimensionPixelSize(R.dimen.stream_ui_text_large),        ),        lastMessageText = TextStyle(            size = resources.getDimensionPixelSize(R.dimen.stream_ui_text_small),        ),        unreadMessageCounterBackgroundColor = Color.BLUE,    )}

These changes should have the following results:


The transformer should be set before the View is rendered to make sure that the new style was applied.

Creating a Custom ViewHolder Factory#

ChannelListView provides a way to completely change the default ViewHolders and add different types of views. All you need to do is to provide your own ChannelListItemViewHolderFactory. Let's see an example that displays the channel's photo, name, and member count.

  1. Create the custom_channel_list_item.xml layout:
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android=""    xmlns:app=""    android:layout_width="match_parent"    android:layout_height="64dp"    >
    <        android:id="@+id/avatarView"        style="@style/StreamUiChannelListAvatarStyle"        android:layout_marginVertical="12dp"        android:layout_marginStart="8dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        />
    <TextView        android:id="@+id/nameTextView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginStart="8dp"        android:ellipsize="end"        android:singleLine="true"        android:textAppearance="@style/StreamUiTextAppearance.BodyBold"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toStartOf="@id/membersCountTextView"        app:layout_constraintHorizontal_chainStyle="spread_inside"        app:layout_constraintStart_toEndOf="@+id/avatarView"        app:layout_constraintTop_toTopOf="parent"        />
    <TextView        android:id="@+id/membersCountTextView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginHorizontal="8dp"        android:textAllCaps="false"        android:textColor="#7A7A7A"        android:textSize="14sp"        android:textStyle="normal"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toEndOf="@id/nameTextView"        app:layout_constraintTop_toTopOf="parent"        />
  1. Add this plurals entry to strings.xml:
<plurals name="members_count">    <item quantity="one">%1d Member</item>    <item quantity="other">%1d Members</item></plurals>
  1. Create a custom ViewHolder and ViewHolder factory:
class CustomChannelListItemViewHolderFactory : ChannelListItemViewHolderFactory() {    override fun createChannelViewHolder(parentView: ViewGroup): BaseChannelListItemViewHolder {        return CustomChannelViewHolder(parentView, listenerContainer.channelClickListener)    }}
class CustomChannelViewHolder(    parent: ViewGroup,    private val channelClickListener: ChannelListView.ChannelClickListener,    private val binding: CustomChannelListItemBinding = CustomChannelListItemBinding.inflate(        LayoutInflater.from(parent.context),        parent,        false    ),) : BaseChannelListItemViewHolder(binding.root) {
    private lateinit var channel: Channel
    init {        binding.root.setOnClickListener { channelClickListener.onClick(channel) }    }
    override fun bind(channel: Channel, diff: ChannelListPayloadDiff) { = channel
        binding.apply {            avatarView.setChannelData(channel)            nameTextView.text = channel.getDisplayName(itemView.context)            membersCountTextView.text = itemView.context.resources.getQuantityString(                R.plurals.members_count,                channel.members.size,                channel.members.size            )        }    }}
  1. Set the custom ViewHolder factory on the ChannelListView:

These changes should have the following results:

Custom ViewHolder

Creating a Custom Loading View#

A custom loading view can be set using the setLoadingView method. Assuming that we have the setup similar to previous steps, we can create a loading view with a shimmer effect by taking the following actions:

  1. Add the Shimmer dependency in your build.gradle file's dependencies block:
implementation "com.facebook.shimmer:shimmer:0.5.0"
  1. Add shape_shimmer.xml into drawable folder:
<?xml version="1.0" encoding="utf-8"?><shape xmlns:android=""    android:shape="rectangle"    >    <solid android:color="#CCCCCC" />    <corners android:radius="20dp" /></shape>
  1. Add a single row layout - item_loading_view.xml into layout folder:
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android=""    xmlns:app=""    android:layout_width="match_parent"    android:layout_height="64dp"    >
    <View        android:id="@+id/avatarPlaceholder"        android:layout_width="40dp"        android:layout_height="40dp"        android:layout_marginStart="8dp"        android:background="@drawable/shape_shimmer"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        />
    <View        android:id="@+id/titlePlaceholder"        android:layout_width="80dp"        android:layout_height="16dp"        android:layout_gravity="center"        android:layout_marginStart="8dp"        android:layout_marginEnd="8dp"        android:background="@drawable/shape_shimmer"        app:layout_constraintStart_toEndOf="@id/avatarPlaceholder"        app:layout_constraintTop_toTopOf="@id/avatarPlaceholder"        />
    <View        android:id="@+id/subtitlePlaceholder"        android:layout_width="0dp"        android:layout_height="16dp"        android:layout_gravity="center"        android:layout_marginStart="8dp"        android:layout_marginTop="8dp"        android:background="@drawable/shape_shimmer"        app:layout_constraintBottom_toBottomOf="@id/avatarPlaceholder"        app:layout_constraintEnd_toStartOf="@+id/datePlaceholder"        app:layout_constraintStart_toEndOf="@id/avatarPlaceholder"        />
    <View        android:id="@+id/datePlaceholder"        android:layout_width="40dp"        android:layout_height="16dp"        android:layout_gravity="center"        android:layout_marginStart="16dp"        android:layout_marginEnd="8dp"        android:background="@drawable/shape_shimmer"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toEndOf="@id/subtitlePlaceholder"        app:layout_constraintTop_toTopOf="@id/subtitlePlaceholder"        />
    <View        android:id="@+id/separator"        android:layout_width="0dp"        android:layout_height="1dp"        android:layout_gravity="center"        android:background="@drawable/shape_shimmer"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        />
  1. Create the final loading view with shimmer effect. Let's call it channel_list_loading_view:
<?xml version="1.0" encoding="utf-8"?><com.facebook.shimmer.ShimmerFrameLayout xmlns:android=""    xmlns:app=""    android:id="@+id/loadingViewContainer"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:layout_marginTop="64dp"    app:shimmer_auto_start="true"    app:shimmer_base_color="#CCCCCC"    app:shimmer_colored="true"    app:shimmer_highlight_color="#FFFFFF"    >
    <LinearLayout        android:layout_width="match_parent"        android:layout_height="match_parent"        android:orientation="vertical"        >
        <include layout="@layout/item_loading_view" />
        <include layout="@layout/item_loading_view" />
        <include layout="@layout/item_loading_view" />
        <include layout="@layout/item_loading_view" />
        <include layout="@layout/item_loading_view" />
        <include layout="@layout/item_loading_view" />
  1. Change ChannelListView's loading view:
// Inflate loading viewval loadingView = LayoutInflater.from(context).inflate(R.layout.channel_list_loading_view, channelListView)// Set loading viewchannelListView.setLoadingView(loadingView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))

These changes should have the following results:

Light mode


When ChannelListViewModel is instantiated it makes a query channels request to get the list of channels. Starting from this point, the list is being synced through WebSocket events. The SDK includes a ChatEventHandler that decides whether to include or exclude the channel from the list after receiving some events. Sometimes desired behavior differs from the default one and you can notice that for example - the channel doesn't disappear from the list after removing a member. In that case, you should provide a custom ChatEventHandler implementation, which has the following interface:

public fun interface ChatEventHandler {    /**     * Function that computes result of handling event. It runs in background.     *     * @param event ChatEvent that may contain updates for the set of channels. See more [ChatEvent]     * @param filter [FilterObject] that can be used to define result of handling.     *     * @return [EventHandlingResult] Result of handling.     */    public fun handleChatEvent(event: ChatEvent, filter: FilterObject): EventHandlingResult}
/** Class representing possible outcome of chat event handling. */public sealed class EventHandlingResult {    /**     * Add a channel to a query channels collection.     *     * @param channel Channel to be added.     */    public class Add(public val channel: Channel) : EventHandlingResult()
    /**     * Remove a channel from a query channels collection.     *     * @param cid cid of channel to remove.     *     */    public class Remove(public val cid: String) : EventHandlingResult()
    /** Skip handling of this event. */    public object Skip : EventHandlingResult()}

You can also use BaseChatEventHandler - a more detailed implementation that covers the most generic cases and forces you to implement the required ones.

To inject the ChatEventHandler to ChannelListViewModel, use ChatEventHandlerFactory inside ChannelListViewModelFactory:

public class ChannelListViewModelFactory @JvmOverloads constructor(    private val filter: FilterObject? = null,    private val sort: QuerySort<Channel> = ChannelListViewModel.DEFAULT_SORT,    private val limit: Int = 30,    private val messageLimit: Int = 1,    private val chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory(),) 

A default ChatEventHandlerFactory implementation provides a ChatEventHandler that updates the ChannelListView based on membership - the channel will be added if the current user is a member and it will be removed otherwise. You can override the factory, like in the example below:

public open class NonMemberChatEventHandlerFactory : ChatEventHandlerFactory() {
    public override fun chatEventHandler(channels: StateFlow<List<Channel>>): ChatEventHandler {        return NonMemberChatEventHandler(channels = channels)    }}

Make sure to pass the factory to the ChannelListViewModel:

private val viewModel: ChannelListViewModel by viewModels {    ChannelListViewModelFactory(        filter = Filters.and(            Filters.eq("type", "messaging"),            Filters.`in`("members", listOf(ChatClient.instance().getCurrentUser()?.id ?: "")),            Filters.or(Filters.notExists("draft"), Filters.eq("draft", false)),        ),        chatEventHandlerFactory = NonMemberChatEventHandlerFactory()    )}

Did you find this page helpful?