Channel List Event Handling

The channel list is updated from two sources:

  • Initial query: Channels matching your filter and sort criteria when the component loads.
  • Real-time events: Channel-related events from the WebSocket connection.

Unlike queries, WebSocket events are not automatically filtered—every channel event is delivered to your app. The SDK's default behavior is membership-based: channels are added when the current user becomes a member and removed when they leave.

When you need custom event handling:

  • You have multiple channel lists showing different channel types (e.g., "public" vs "private")
  • You want to add channels based on criteria other than membership
  • You need to prevent certain events from updating the list

ChatEventHandler Interface

The ChatEventHandler is a functional interface that determines how chat events affect the channel list:

public fun interface ChatEventHandler {
    public fun handleChatEvent(
        event: ChatEvent,
        filter: FilterObject,
        cachedChannel: Channel?
    ): EventHandlingResult
}
  • event: The incoming chat event (e.g., new message, member added/removed, channel updated).
  • filter: The filter associated with the channel list query.
  • cachedChannel: The cached channel object if available, useful for checking current state.

EventHandlingResult

Every event handler must return an EventHandlingResult that tells the SDK what action to take:

public sealed class EventHandlingResult {
    public data class Add(val channel: Channel) : EventHandlingResult()
    public data class WatchAndAdd(val cid: String) : EventHandlingResult()
    public data class Remove(val cid: String) : EventHandlingResult()
    public data object Skip : EventHandlingResult()
}
  • Add(channel): Immediately adds the channel to the list. Use when the event contains full channel data.
  • WatchAndAdd(cid): Watches the channel first to fetch its data, then adds it. Use when you only have the channel ID.
  • Remove(cid): Removes the channel from the list.
  • Skip: Ignores the event; no changes to the list.

Default Behavior

The DefaultChatEventHandler implements membership-based logic. Here's what it does for common events:

EventAction
NewMessageEventAdds channel if not a system message
NotificationMessageNewEventWatches and adds the channel
NotificationAddedToChannelEventWatches and adds the channel
MemberAddedEventAdds channel if current user joined
MemberRemovedEventRemoves channel if current user left
NotificationRemovedFromChannelEventRemoves channel if current user left
ChannelDeletedEventRemoves the channel
ChannelHiddenEventRemoves the channel
Other eventsSkips (no change)

Custom ChatEventHandler

The default event handling logic might not fit your use case. You can provide a custom ChatEventHandler by extending one of the base classes:

  • DefaultChatEventHandler - Extend this when you need membership-based behavior with additional logic. Best for most customizations.
  • BaseChatEventHandler - Extend this when you need non-membership-based logic but still want the convenience of separate handleChannelEvent and handleCidEvent methods.
  • ChatEventHandler - Implement this directly when you need full control over all event types.

The ChatEventHandlerFactory.chatEventHandler() method receives a StateFlow<Map<String, Channel>?> representing the currently visible channels. Use this to check if a channel is already in the list before deciding whether to add or skip it.

How to provide a custom handler:

  • UI Components (XML): Pass chatEventHandlerFactory to ChannelListViewModelFactory
  • Compose: Pass chatEventHandlerFactory to ChannelViewModelFactory
  • Without UI SDK: Pass a custom handler to ChatClient.queryChannelsAsState()

Multiple Channel Lists

If your application displays multiple channel lists (e.g., one for "public" channels and one for "private" channels), each list needs its own ChatEventHandler to filter events by channel type.

Without custom handlers, both lists would update whenever any channel event arrives, regardless of channel type.

Creating a Channel Type Filter

Create a reusable handler that filters events by channel type prefix:

class ChannelTypeChatEventHandler(
    private val channelTypePrefix: String,
    channels: StateFlow<Map<String, Channel>?>,
    clientState: ClientState,
) : DefaultChatEventHandler(channels, clientState) {

    override fun handleChannelEvent(event: HasChannel, filter: FilterObject): EventHandlingResult {
        return if (event.channel.cid.startsWith(channelTypePrefix)) {
            super.handleChannelEvent(event, filter)
        } else {
            EventHandlingResult.Skip
        }
    }

    override fun handleCidEvent(
        event: CidEvent,
        filter: FilterObject,
        cachedChannel: Channel?,
    ): EventHandlingResult {
        return if (event.cid.startsWith(channelTypePrefix)) {
            super.handleCidEvent(event, filter, cachedChannel)
        } else {
            EventHandlingResult.Skip
        }
    }
}

class ChannelTypeChatEventHandlerFactory(
    private val channelTypePrefix: String,
) : ChatEventHandlerFactory() {
    override fun chatEventHandler(channels: StateFlow<Map<String, Channel>?>): ChatEventHandler {
        return ChannelTypeChatEventHandler(channelTypePrefix, channels, ChatClient.instance().clientState)
    }
}

Applying the Handler

Use the factory when creating your ViewModels.

Compose UI Components:

// For public channels list
val publicChannelsFactory = ChannelViewModelFactory(
    chatEventHandlerFactory = ChannelTypeChatEventHandlerFactory("public")
)

// For private channels list
val privateChannelsFactory = ChannelViewModelFactory(
    chatEventHandlerFactory = ChannelTypeChatEventHandlerFactory("private")
)

XML UI Components:

// For public channels list
val publicChannelsFactory = ChannelListViewModelFactory(
    chatEventHandlerFactory = ChannelTypeChatEventHandlerFactory("public")
)

// For private channels list
val privateChannelsFactory = ChannelListViewModelFactory(
    chatEventHandlerFactory = ChannelTypeChatEventHandlerFactory("private")
)

With this approach, each channel list only handles events for its channel type. The public list ignores private channel events and vice versa.

You can adapt this pattern for other use cases like channel categories, groups, or custom metadata.