Thread List

ThreadListView is an XML UI component which shows an overview of all threads of which the user is a member of. It shows information about the channel, the thread parent message, the most recent reply in the thread, and the number of unread replies.

The component is paginated by default, and only the most recently updated threads are loaded initially. Older threads are loaded only when the user scrolls to the end of the thread list.

While this component is visible, and a new thread is created, or a thread which is not yet loaded is updated, the component will show a banner informing the user about the number of new threads, which the user can then click, to reload the thread list and load the newly updated threads.

The ThreadListView component is available on the UI components SDK since version 6.8.0.

Usage

The component can easily be used directly in an XML layout:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <!-- Other views... -->

    <io.getstream.chat.android.ui.feature.threads.list.ThreadListView
        android:id="@+id/threadListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <!-- Other views... -->

</androidx.constraintlayout.widget.ConstraintLayout>

You can use the ThreadListViewModel as a convenient way to populate the data for the ThreadListView. To instantiate such ViewModel, you need to first create an instance of ThreadsViewModelFactory:

private val threadsViewModelFactory by lazy {
    ThreadsViewModelFactory(
        threadLimit = /* ... */,
        threadReplyLimit = /* ... */,
        threadParticipantLimit = /* ... */
    )
}

The ThreadsViewModelFactory accepts three configurable parameters:

  • threadLimit - The maximum number of threads to be loaded per page (default: 25).
  • threadReplyLimit - The maximum number of (latest) replies to be loaded per thread (default: 10).
  • threadParticipantLimit - The maximum number of participants to be loaded per thread (default: 10).

After the ThreadsViewModelFactory is configured, you can instantiate the ThreadListViewModel, and bind it to the ThreadListView via the bindView method.

private val viewModel: ThreadListViewModel by viewModels {
    threadsViewModelFactory
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel.bindView(binding.threadListView, viewLifecycleOwner)
    // Other views setup...
}

A ThreadListView bound to the ThreadListViewModel via the bindView method will produce a fully working thread list, together with a loading state, and an empty state for the case without threads:

No threads
Empty
Thread list + new threads banner
Loaded

The bindView method sets listeners on the View and the ViewModel. Any additional listeners should be set after calling bindView.

Handling actions

The ThreadListView exposes several configurable user actions:

  • setThreadClickListener - Sets the click listener of the thread items in the list. By default, this action is empty, so you need to provide your own ThreadListView.ThreadClickListener to handle clicks on the items:

      threadListView.setThreadClickListener { thread: Thread ->
          // Handle click on the given Thread
      }
  • setUnreadThreadsBannerClickListener - Sets the click listener of the unread threads banner. By default, clicking the banner results in reloading the whole thread list.

      threadListView.setUnreadThreadsBannerClickListener {
          // Note: By default, clicking the banner results in reloading the
          // whole thread list.
          // Make sure to call viewModel::load if you want to keep the
          // default behaviour in addition to adding your own.
          viewModel.load()
          // Additional action to perfrom on unread threads banner click
      }
  • setLoadMoreListener - Sets the listener for reaching the end of the thread list, indicating that more threads should be loaded. By default, this will load the next page of threads (if available).

      binding.threadListView.setLoadMoreListener {
          // Note: By default, reaching the end of the lists results in loading
          // the next page of threads.
          // Make sure to call viewModel::loadNextPage if you want to keep the
          // default behaviour in addition to adding your own.
          viewModel.loadNextPage()
          // Additional action to perfrom on reaching the end of the list
      }

    The ThreadListView also exposes several methods which allow you to directly modify the data displayed in the view:

  • showThreads(threads: List<Thread>, isLoadingMore: Boolean) - Overrides the currently shown thread list with the passed List<Thread>.

  • showLoading() - Clears the currently shown thread list and shows the loading state of the list.

  • showUnreadThreadsBanner(unreadThreadsCount: Int) - Shows the unread threads banner.

These methods are used internally in the bindView method, so you should use them only if you are NOT using the bindView method to configure the ThreadListView. Using bindView and calling any of these methods might result in unexpected behaviour. We recommend to always use the bindView method and let the data shown by the ThreadListView be managed by the ThreadListViewModel.

Customization

There are multiple ways for customizing the apperance of the thread list:

Custom attributes

The ThreadListView exposes multiple attributes that can be overridden to customize specific part of the list. For example, you can override the app:streamUiThreadListBackground to provide a custom background color for the thread list:

<io.getstream.chat.android.ui.feature.threads.list.ThreadListView
        android:id="@+id/threadListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:streamUiThreadListBackground="@color/app_background"
        />

Custom theme

A different way customize to the ThreadListView is to create a custom theme extending from the StreamUi.ThreadList style:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <!-- Set the streamUiTheme to the custom StreamTheme -->
    <item name="streamUiTheme">@style/StreamTheme</item>
</style>

<style name="StreamTheme" parent="@style/StreamUiTheme">
    <!-- Set the streamUiThreadList style to the custom ThreadListTheme -->
    <item name="streamUiThreadListStyle">@style/ThreadListTheme</item>
</style>

<style name="ThreadListTheme" parent="StreamUi.ThreadList">
    <!-- Other ThreadList properties... -->
    <item name="streamUiThreadListBackground">@color/app_background</item>
</style>

The list of configurable properties via custom StreamUi.ThreadList theme (or by using custom ThreadListView attributes) includes:

<!-- General customization -->
<!-- Background of the list -->
<item name="streamUiThreadListBackground">@color/stream_ui_white_snow</item>

<!-- Empty state customization -->
<!-- Drawable shown when there are no threads -->
<item name="streamUiThreadListEmptyStateDrawable">@drawable/stream_ui_ic_threads_empty</item>
 <!-- Text shown when there are no threads -->
<item name="streamUiThreadListEmptyStateText">@string/stream_ui_thread_list_empty_title</item>
 <!-- Color of the text shown when there are no threads -->
<item name="streamUiThreadListEmptyStateTextColor">@color/stream_ui_text_color_secondary</item>
 <!-- Size of the text shown when there are no threads -->
<item name="streamUiThreadListEmptyStateTextSize">@dimen/stream_ui_text_large</item>
 <!-- Style of the text shown when there are no threads (normal/bold/italic) -->
<item name="streamUiThreadListEmptyStateTextStyle">normal</item>

<!-- Results customization -->
<!-- Drawable for the thread icon -->
<item name="streamUiThreadListThreadIconDrawable">@drawable/stream_ui_ic_thread</item>
<!-- Color of the thread title -->
<item name="streamUiThreadListThreadTitleTextColor">@color/stream_ui_text_color_primary</item>
<!-- Size of the thread title -->
<item name="streamUiThreadListThreadTitleTextSize">@dimen/stream_ui_text_medium</item>
<!-- Style of the thread title (normal/bold/italic) -->
<item name="streamUiThreadListThreadTitleTextStyle">bold</item>
<!-- Color of the thread parent message text -->
<item name="streamUiThreadListThreadReplyToTextColor">@color/stream_ui_text_color_secondary</item>
<!-- Size of the thread parent message text -->
<item name="streamUiThreadListThreadReplyToTextSize">@dimen/stream_ui_text_medium</item>
<!-- Style of the thread parent message text (normal/bold/italic) -->
<item name="streamUiThreadListThreadReplyToTextStyle">normal</item>
<!-- Color of the latest reply sender name -->
<item name="streamUiThreadListThreadLatestReplySenderTextColor">@color/stream_ui_text_color_primary</item>
<!-- Size of the latest reply sender name -->
<item name="streamUiThreadListThreadLatestReplySenderTextSize">@dimen/stream_ui_text_medium</item>
<!-- Style of the latest reply sender name (normal/bold/italic) -->
<item name="streamUiThreadListThreadLatestReplySenderTextStyle">bold</item>
<!-- Color of the latest reply message text -->
<item name="streamUiThreadListThreadLatestReplyMessageTextColor">@color/stream_ui_text_color_secondary</item>
<!-- Size of the latest reply message text -->
<item name="streamUiThreadListThreadLatestReplyMessageTextSize">@dimen/stream_ui_text_medium</item>
<!-- Style of the latest reply message text (normal/bold/italic) -->
<item name="streamUiThreadListThreadLatestReplyMessageTextStyle">normal</item>
<!-- Color of the latest reply timestamp -->
<item name="streamUiThreadListThreadLatestReplyTimeTextColor">@color/stream_ui_text_color_secondary</item>
<!-- Size of the latest reply timestamp -->
<item name="streamUiThreadListThreadLatestReplyTimeTextSize">@dimen/stream_ui_text_medium</item>
<!-- Style of the latest reply timestamp (normal/bold/italic) -->
<item name="streamUiThreadListThreadLatestReplyTimeTextStyle">normal</item>
<!-- Color of the unread count badge text -->
<item name="streamUiThreadListThreadUnreadCountBadgeTextColor">@color/stream_ui_literal_white</item>
<!-- Size of the unread count badge text -->
<item name="streamUiThreadListThreadUnreadCountBadgeTextSize">@dimen/stream_ui_text_small</item>
<!-- Style of the unread count badge text (normal/bold/italic) -->
<item name="streamUiThreadListThreadUnreadCountBadgeTextStyle">normal</item>
<!-- Background of the unread count badge -->
<item name="streamUiThreadListThreadUnreadCountBadgeBackground">@drawable/stream_ui_shape_badge_background</item>

<!-- Unread Threads Banner customization -->
<!-- Color of the banner text -->
<item name="streamUiThreadListUnreadThreadsBannerTextColor">@color/stream_ui_white</item>
<!-- Size of the banner text -->
<item name="streamUiThreadListUnreadThreadsBannerTextSize">@dimen/stream_ui_text_large</item>
<!-- Style of the banner text (normal/bold/italic) -->
<item name="streamUiThreadListUnreadThreadsBannerTextStyle">normal</item>
<!-- Icon shown at the end of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerIcon">@drawable/stream_ui_ic_union</item>
<!-- Background of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerBackground">@drawable/stream_ui_shape_unread_threads_banner</item>
<!-- Left padding of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerPaddingLeft">@dimen/stream_ui_spacing_medium</item>
<!-- Top padding of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerPaddingTop">@dimen/stream_ui_spacing_medium</item>
<!-- Right padding of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerPaddingRight">@dimen/stream_ui_spacing_medium</item>
<!-- Bottom padding of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerPaddingBottom">@dimen/stream_ui_spacing_medium</item>
<!-- Left margin of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerMarginLeft">@dimen/stream_ui_spacing_small</item>
<!-- Top margin of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerMarginTop">@dimen/stream_ui_spacing_small</item>
<!-- Right margin of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerMarginRight">@dimen/stream_ui_spacing_small</item>
<!-- Bottom margin of the banner -->
<item name="streamUiThreadListUnreadThreadsBannerMarginBottom">@dimen/stream_ui_spacing_small</item>

Style transformer

Another way of customizing the ThreadListView is by providing a custom StyleTransformer for modifying the default ThreadListViewStyle:

TransformStyle.threadListViewStyle = StyleTransformer { defaultStyle ->
    val backgroundColor = ContextCompat.getColor(context, R.color.app_background)
    defaultStyle.copy(
        backgroundColor = backgroundColor,
        // Override other attributes...
    )
}

ThreadListItemViewHolderFactory

If the style customization is not enough for your needs, you can also customize the ThreadListView by overriding the ThreadListItemViewHolderFactory. By providing a custom implementation of ThreadListItemViewHolderFactory, you can override the default list items, or even add new types of ViewHolders. For example, to provide a custom ViewHolder for rendering ThreadListItem.ThreadItem, you would need to do the following steps:

  • implement a custom ViewHolder by extending the BaseThreadListItemViewHolder
  • override the ThreadListItemViewHolderFactory.getItemViewType(BaseThreadListItemViewHolder) to ensure that the custom ViewHolder is properly mapped to the corresponding item type
  • override the ThreadListItemViewHolderFactory.createThreadItemViewHolder so that it returns the custom ViewHolder
  • set the new ThreadListItemViewHolderFactory in the ThreadListView
// Create custom ViewHolder
class CustomThreadViewHolder(
    parentView: ViewGroup,
    val clickListener: ThreadListView.ThreadClickListener?,
    val binding: CustomThreadItemBinding = CustomThreadItemBinding.inflate(
        LayoutInflater.from(parentView.context),
        parentView,
        false
    ),
): BaseThreadListItemViewHolder<ThreadListItem.ThreadItem>(binding.root) {
    override fun bind(item: ThreadListItem.ThreadItem) {
        // Custom bind logic
    }
}

// Create custom ViewHolderFactory
class CustomThreadItemViewHolderFactory : ThreadListItemViewHolderFactory() {

    // Ensure the new viewHodler type is mapped to the proper type
    override fun getItemViewType(
      viewHolder: BaseThreadListItemViewHolder<out ThreadListItem>
    ): Int {
        return when (viewHolder) {
            is CustomThreadViewHolder -> ThreadListItemViewType.ITEM_THREAD
            else -> super.getItemViewType(viewHolder)
        }
    }

    // Ensure the custom ViewHolder is returned from its factory method.
    override fun createThreadItemViewHolder(
      parentView: ViewGroup
    ): BaseThreadListItemViewHolder<ThreadListItem.ThreadItem> {
        return CustomThreadViewHolder(parentView, clickListener)
    }
}

// Set the ViewHolderFactory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.threadListView.setViewHolderFactory(CustomThreadItemViewHolderFactory())
    viewModel.bindView(binding.threadListView, viewLifecycleOwner)
    // Other setup...
}

If you would like to indtroduce new types of ViewHolders in attidion to the existing ones, you would need to do the the following steps:

  • implement a custom ViewHolder by extending the BaseThreadListItemViewHolder
  • override the ThreadListItemViewHolderFactory.getItemViewType(ThreadListItem) to ensure that the custom ViewHolder is properly mapped to the corresponding item type
  • override the ThreadListItemViewHolderFactory.getItemViewType(BaseThreadListItemViewHolder) to ensure that the custom ViewHolder is properly mapped to the corresponding item type
  • override the ThreadListItemViewHolderFactory.createViewHolder so that the new item type is properly mapped to the new ViewHolder
  • set the new ThreadListItemViewHolderFactory in the ThreadListView
// Create the custom ViewHolder type
class SingleReplyThreadItemViewHolder(
    parentView: ViewGroup,
    val binding: SingleReplyThreadItemBinding = SingleReplyThreadItemBinding.inflate(
        LayoutInflater.from(parentView.context),
        parentView,
        false
    ),
): BaseThreadListItemViewHolder<ThreadListItem.ThreadItem>(binding.root) {
    override fun bind(item: ThreadListItem.ThreadItem) {
        // Custom bind logic
    }
}

// Create custom ViewHolderFactory
class CustomThreadItemViewHolderFactory : ThreadListItemViewHolderFactory() {

    companion object {
        // Define the new item type
        private const val SINGLE_REPLY_THREAD_ITEM_TYPE = 1001
    }

    // Ensure the new type is properly resolved
    override fun getItemViewType(item: ThreadListItem): Int {
        return when (item) {
            is ThreadListItem.ThreadItem -> {
                if (item.thread.replyCount == 1) {
                    SINGLE_REPLY_THREAD_ITEM_TYPE
                } else {
                    super.getItemViewType(item)
                }
            }
            else -> super.getItemViewType(item)
        }
    }

    // Ensure the new viewHodler type is mapped to the proper type
    override fun getItemViewType(
      viewHolder: BaseThreadListItemViewHolder<out ThreadListItem>
    ): Int {
        return when (viewHolder) {
            is SingleReplyThreadItemViewHolder -> SINGLE_REPLY_THREAD_ITEM_TYPE
            else -> super.getItemViewType(viewHolder)
        }
    }

    // Map the new ViewHolder to the new item type
    override fun createViewHolder(
        parentView: ViewGroup,
        viewType: Int
    ): BaseThreadListItemViewHolder<out ThreadListItem> {
        return when (viewType) {
            SINGLE_REPLY_THREAD_ITEM_TYPE -> SingleReplyThreadItemViewHolder(parentView)
            else -> super.createViewHolder(parentView, viewType)
        }
    }
}

// Set the ViewHolderFactory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.threadListView.setViewHolderFactory(CustomThreadItemViewHolderFactory())
    viewModel.bindView(binding.threadListView, viewLifecycleOwner)
    // Other setup...
}
© Getstream.io, Inc. All Rights Reserved.