# Channels State and Filtering

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:

```kotlin
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:

```kotlin
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:

| Event                                 | Action                               |
| ------------------------------------- | ------------------------------------ |
| `NewMessageEvent`                     | Adds channel if not a system message |
| `NotificationMessageNewEvent`         | Watches and adds the channel         |
| `NotificationAddedToChannelEvent`     | Watches and adds the channel         |
| `MemberAddedEvent`                    | Adds channel if current user joined  |
| `MemberRemovedEvent`                  | Removes channel if current user left |
| `NotificationRemovedFromChannelEvent` | Removes channel if current user left |
| `ChannelDeletedEvent`                 | Removes the channel                  |
| `ChannelHiddenEvent`                  | Removes the channel                  |
| Other events                          | Skips (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.

<admonition type="note">

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.

</admonition>

**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:

<tabs>

<tabs-item value="kotlin" label="Kotlin">

```kotlin
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)
    }
}
```

</tabs-item>

<tabs-item value="java" label="Java">

```java
public final class ChannelTypeChatEventHandler extends DefaultChatEventHandler {
    private final String channelTypePrefix;

    public ChannelTypeChatEventHandler(
            @NonNull String channelTypePrefix,
            @NonNull StateFlow<? extends Map<String, Channel>> channels,
            @NonNull ClientState clientState
    ) {
        super(channels, clientState);
        this.channelTypePrefix = channelTypePrefix;
    }

    @NonNull
    @Override
    public EventHandlingResult handleChannelEvent(@NonNull HasChannel event, @NonNull FilterObject filter) {
        if (event.getChannel().getCid().startsWith(channelTypePrefix)) {
            return super.handleChannelEvent(event, filter);
        } else {
            return EventHandlingResult.Skip.INSTANCE;
        }
    }

    @NonNull
    @Override
    public EventHandlingResult handleCidEvent(@NonNull CidEvent event, @NonNull FilterObject filter, @Nullable Channel cachedChannel) {
        if (event.getCid().startsWith(channelTypePrefix)) {
            return super.handleCidEvent(event, filter, cachedChannel);
        } else {
            return EventHandlingResult.Skip.INSTANCE;
        }
    }
}

public final class ChannelTypeChatEventHandlerFactory extends ChatEventHandlerFactory {
    private final String channelTypePrefix;

    public ChannelTypeChatEventHandlerFactory(@NonNull String channelTypePrefix) {
        this.channelTypePrefix = channelTypePrefix;
    }

    @NonNull
    @Override
    public ChatEventHandler chatEventHandler(@NonNull StateFlow<? extends Map<String, Channel>> channels) {
        return new ChannelTypeChatEventHandler(channelTypePrefix, channels, ChatClient.instance().getClientState());
    }
}
```

</tabs-item>

</tabs>

### Applying the Handler

Use the factory when creating your ViewModels.

**Compose UI Components:**

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

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

**XML UI Components:**

<tabs>

<tabs-item value="kotlin" label="Kotlin">

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

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

</tabs-item>

<tabs-item value="java" label="Java">

```java
// For public channels list
ChannelListViewModelFactory publicChannelsFactory = new ChannelListViewModelFactory(
        null, // filter
        ChannelListViewModel.DEFAULT_SORT,
        30, // limit
        1,  // messageLimit
        30, // memberLimit
        new ChannelTypeChatEventHandlerFactory("public")
);

// For private channels list
ChannelListViewModelFactory privateChannelsFactory = new ChannelListViewModelFactory(
        null, // filter
        ChannelListViewModel.DEFAULT_SORT,
        30, // limit
        1,  // messageLimit
        30, // memberLimit
        new ChannelTypeChatEventHandlerFactory("private")
);
```

</tabs-item>

</tabs>

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.


---

This page was last updated at 2026-04-17T17:33:31.021Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/v6/client/guides/channels/](https://getstream.io/chat/docs/sdk/android/v6/client/guides/channels/).