<io.getstream.chat.android.ui.channel.list.ChannelListView
android:id="@+id/channelsView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
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.
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.
Usage
To use ChannelListView
, include it in your XML layout as shown below:
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 ViewModel
val viewModel: ChannelListViewModel by viewModels {
ChannelListViewModelFactory(
filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf(ChatClient.instance().getCurrentUser()!!.id)),
),
sort = ChannelListViewModel.DEFAULT_SORT,
limit = 30,
)
}
// Bind the ViewModel with ChannelListView
viewModel.bindView(channelListView, viewLifecycleOwner)
// Instantiate the ViewModel
FilterObject filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.in("members", Collections.singletonList(ChatClient.instance().getCurrentUser().getId()))
);
ViewModelProvider.Factory factory = new ChannelListViewModelFactory.Builder()
.filter(filter)
.sort(ChannelListViewModel.DEFAULT_SORT)
.limit(30)
.build();
ChannelListViewModel viewModel = new ViewModelProvider(this, factory).get(ChannelListViewModel.class);
// Bind the ViewModel with ChannelListView
ChannelListViewModelBinding.bind(viewModel, channelListView, getViewLifecycleOwner());
You may need to pass a custom ChatEventHandler
to make sure the list is updated properly. Check ChannelListUpdates section 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 Mode | Dark 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
}
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.
Customization
There are two ways to customize the appearance of ChannelListView
:
- Using XML attributes
- Using the
TransformStyle
API at runtime to customize the style of allChannelListView
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,
)
}
TransformStyle.setChannelListStyleTransformer(source -> {
// Customize the style
return source;
});
These changes should have the following results:
Before | After |
---|---|
The transformer should be set before the View is rendered to make sure that the new style was applied.
Customizing Swipe Actions
By default ChannelListView
supports two swipe actions:
More Options
- available for every channel.Delete
- available for the channels whereChannel.ownCapbilities
contains"delete-channel"
Here is the default implementation:
Swipe Actions |
---|
You can customize the swipe actions as shown below.
Customizing Icons
channelsView.setMoreOptionsIconProvider { channel ->
// You can generate the icon Drawable based on the channel object
ContextCompat.getDrawable(context, R.drawable.custom_action_icon_more)
}
channelsView.setDeleteOptionIconProvider { channel ->
// You can generate the icon Drawable based on the channel object
ContextCompat.getDrawable(context, R.drawable.custom_action_icon_delete)
}
channelsView.setDeleteOptionIconProvider(channel -> {
// You can generate icon Drawable based on the channel object
return ContextCompat.getDrawable(context, R.drawable.custom_action_icon_more);
});
channelsView.setDeleteOptionIconProvider(channel -> {
// You can generate icon Drawable based on the channel object
return ContextCompat.getDrawable(context, R.drawable.custom_action_icon_delete);
});
Customizing Visibility
channelsView.setIsMoreOptionsVisible { channel ->
// You can determine visibility based on the channel object.
true
}
channelsView.setIsDeleteOptionVisible { channel ->
// You can determine visibility based on the channel object.
// Here is the default implementation:
channel.ownCapabilities.contains("delete-channel")
}
channelsView.setIsMoreOptionsVisible(channel -> {
// you generate icon based on the channel object
return true;
});
channelsView.setIsDeleteOptionVisible(channel -> {
// You can determine visibility based on the channel object.
// Here is the default implementation:
return channel.getOwnCapabilities().contains("delete-channel");
});
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.
- Create the
custom_channel_list_item.xml
layout:
<?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="64dp"
>
<io.getstream.chat.android.ui.avatar.AvatarView
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"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- Add this plurals entry to
strings.xml
:
<plurals name="members_count">
<item quantity="one">%1d Member</item>
<item quantity="other">%1d Members</item>
</plurals>
- 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) {
this.channel = channel
binding.apply {
avatarView.setChannelData(channel)
nameTextView.text = ChatUI.channelNameFormatter.formatChannelName(
channel = channel,
currentUser = ChatClient.instance().getCurrentUser()
)
membersCountTextView.text = itemView.context.resources.getQuantityString(
R.plurals.members_count,
channel.members.size,
channel.members.size
)
}
}
}
public final class CustomChannelListItemViewHolderFactory extends ChannelListItemViewHolderFactory {
@Override
public int getItemViewType(@NonNull ChannelListItem item) {
// Override together with createViewHolder() to introduce different view holder types
return super.getItemViewType(item);
}
@NonNull
@Override
public BaseChannelListItemViewHolder createViewHolder(@NonNull ViewGroup parentView, int viewType) {
// Override to create custom create view holder logic
return super.createViewHolder(parentView, viewType);
}
@NonNull
@Override
protected BaseChannelListItemViewHolder createChannelViewHolder(@NonNull ViewGroup parentView) {
// Create custom channel view holder
return super.createChannelViewHolder(parentView);
}
@NonNull
@Override
protected BaseChannelListItemViewHolder createLoadingMoreViewHolder(@NonNull ViewGroup parentView) {
// Create custom loading more view holder
return super.createLoadingMoreViewHolder(parentView);
}
}
- Set the custom ViewHolder factory on the
ChannelListView
:
// Create custom view holder factory
val customFactory = CustomChannelListItemViewHolderFactory()
// Set custom view holder factory
channelListView.setViewHolderFactory(customFactory)
// Create custom view holder factory
CustomChannelListItemViewHolderFactory customFactory = new CustomChannelListItemViewHolderFactory();
// Set custom view holder factory
channelListView.setViewHolderFactory(customFactory);
These changes should have the following results:
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:
- Add the Shimmer dependency in your
build.gradle
file’s dependencies block:
implementation "com.facebook.shimmer:shimmer:0.5.0"
- Add
shape_shimmer.xml
into drawable folder:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
>
<solid android:color="#CCCCCC" />
<corners android:radius="20dp" />
</shape>
- 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="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- 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="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
- Change
ChannelListView
’s loading view:
// Inflate loading view
val loadingView = LayoutInflater.from(context).inflate(R.layout.channel_list_loading_view, channelListView)
// Set loading view
channelListView.setLoadingView(loadingView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
// Inflate loading view
View loadingView = LayoutInflater.from(getContext()).inflate(R.layout.channel_list_loading_view, channelListView);
// Set loading view
channelListView.setLoadingView(loadingView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
These changes should have the following results: