# State Layer

The `StatePlugin` manages reactive chat state — channels, messages, typing indicators, read receipts, and more. It automatically updates when data changes, whether from user actions or real-time server events.

## Plugin setup

The `StatePlugin` is published as a separate artifact - `stream-chat-android-state`. This artifact is automatically included as a transitive dependency when using our XML or Compose UI Components. If you are building your own UI layer without using our UI components, you need to add the following dependency to your `build.gradle.kts` file:

```kotlin
implementation("io.getstream:stream-chat-android-state:<latest-version>")
```

To use the `StatePlugin`, you need to create an instance of `StreamStatePluginFactory`, and register it with the `ChatClient` during its initialization:

<tabs>

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

```kotlin
// Create a state plugin factory
val statePluginFactory = StreamStatePluginFactory(
    config = StatePluginConfig(
        // Enables/disables background synchronization when push notifications are received
        backgroundSyncEnabled = true,
        // Enables/disables tracking online states for users
        userPresence = true,
        // Enables/disables automatic sync on reconnect
        isAutomaticSyncOnReconnectEnabled = true,
        // Sets the maximum age threshold for pending local operations before they are discarded
        syncMaxThreshold = TimeDuration.hours(12),
        // Function to provide the current time in milliseconds since epoch
        now = { System.currentTimeMillis() },
        // Configuration for message limits in memory
        messageLimitConfig = MessageLimitConfig(
            // Set a limit of 500 messages for `livestream` channels
            channelMessageLimits = setOf(ChannelMessageLimit("livestream", 500)),
        ),
    ),
    appContext = context
)

ChatClient.Builder(apiKey, context)
    // Add the state plugin to the chat client
    .withPlugins(statePluginFactory)
    .build()
```

</tabs-item>

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

```java
// Enables/disables background synchronization when push notifications are received
boolean backgroundSyncEnabled = true;
// Enables/disables tracking online states for users
boolean userPresence = true;
// Enables/disables automatic sync on reconnect
boolean isAutomaticSyncOnReconnectEnabled = true;
// Sets the maximum age threshold for pending local operations before they are discarded
TimeDuration syncMaxThreshold = TimeDuration.Companion.hours(12);
// Function to provide the current time in milliseconds since epoch
Function0<Long> now = System::currentTimeMillis;
// Configuration for message limits in memory
Set<ChannelMessageLimit> channelMessageLimits = new HashSet<>();
channelMessageLimits.add(new ChannelMessageLimit("livestream", 500));
MessageLimitConfig messageLimitConfig = new MessageLimitConfig(channelMessageLimits);

// Create a state plugin factory
StreamStatePluginFactory statePluginFactory = new StreamStatePluginFactory(
        new StatePluginConfig(
                backgroundSyncEnabled,
                userPresence,
                isAutomaticSyncOnReconnectEnabled,
                syncMaxThreshold,
                now,
                messageLimitConfig
        ),
        context.getApplicationContext()
);

new ChatClient.Builder(apiKey, context)
        // Add the state plugin to the chat client
        .withPlugins(statePluginFactory)
        .build();
```

</tabs-item>

</tabs>

The `StatePluginConfig` allows you to configure the following options:

- **backgroundSyncEnabled**: Controls whether the SDK performs background synchronization when push notifications are received. When enabled (default: `true`), the SDK automatically syncs messages in the background when a push notification arrives, ensuring the local state/database stays up-to-date even when the app is in the background. This is particularly useful for displaying accurate notification content and maintaining offline state consistency. Disable this if you want to reduce background processing.

<admonition type="warning">
The background sync on push notification flag (<b>backgroundSyncEnabled</b>) is no longer needed to keep the state in sync and will be removed in the future. If you are using the default UI components, or building your own UI using the <b>ChatClient.queryChannelsAsState</b> / <b>ChatClient.watchChannelAsState</b> methods, the state will always be up-to-date. We recommend disabling it to avoid unnecessary background work.
</admonition>

- **userPresence**: Controls whether the SDK subscribes to and processes user presence events (online/offline status, last active time). When enabled (default: `true`), the SDK receives real-time updates about user presence changes and updates the user objects in channels, members, and watchers accordingly. This affects both WebSocket event subscriptions and the `presence` parameter in API requests. Disabling this can reduce network traffic and processing overhead if your application doesn't need to display user online/offline status.
- **isAutomaticSyncOnReconnectEnabled**: Specifies if local data is updated with subscribing to web-socket events after reconnection. When turning this off, it is up for SDK user to observe web-socket connection state for reconnection and syncing data by calling, for example `ChatClient.queryChannels`, or the `StatePlugin` methods such as `ChatClient.queryChannelsAsState`/`ChatClient.watchChannelAsState`.
- **syncMaxThreshold**: The maximum age threshold for pending local operations (channels, messages, reactions) before they are considered too old to retry and are discarded. Default is 12 hours. When the SDK attempts to retry failed operations (e.g., sending a message, creating a channel, adding a reaction) upon reconnection, it checks if the operation's timestamp (createdLocallyAt, updatedLocallyAt, deletedAt, or createdAt) exceeds this threshold. If it does, the operation is removed from the local database instead of being retried, preventing the SDK from attempting to sync stale operations that are no longer relevant.
- **now**: A function that provides the current time in milliseconds since epoch (Unix timestamp). Defaults to `System.currentTimeMillis`. This is used throughout the state plugin for time-based operations such as calculating sync thresholds. In general, this is not something that needs to be overridden unless you have specific requirements for time handling in your application.
- **messageLimitConfig**: Configuration that controls the maximum number of messages kept in memory for different channel types. This helps manage memory usage in channels with large message histories. When the number of messages exceeds the configured limit (plus a buffer), older messages are automatically trimmed from the in-memory state. By default, no limits are applied, meaning all messages are kept in memory. See `MessageLimitConfig` and `ChannelMessageLimit` for configuration details.

## Accessing state

If you are using our XML or Compose UI Components - no further setup is required. The UI components will use the `StatePlugin` internally to render the chat state.

If you are building your own UI layer, you can access the state using the extension functions provided by the `StatePlugin`. These functions allow you to query channels, watch channels and watch threads as state objects that can be observed for changes.

### Query channels

To fetch a list of channels, while also receiving real-time updates for it, the state layer provides the `ChatClient.queryChannelsAsState` extension function. It accepts a `QueryChannelsRequest` specifying the query criteria, and it returns a `StateFlow` of `QueryChannelsState`. You can collect this state flow to observe changes to the list of channels matching the query.

```kotlin
// Define the query request
val request = QueryChannelsRequest(
    filter = Filters.and(
        Filters.eq("type", "messaging"),
        Filters.`in`("members", listOf(currentUserId)),
    ),
    limit = 10,
    querySort = QuerySortByField.descByName("lastMessageAt")
).apply {
    watch = true
    state = true
}

// Obtain the StateFlow for the query channels state
val channelsStateFlow: StateFlow<QueryChannelsState?> =
    chatClient.queryChannelsAsState(request)

// Collect the state flow to observe changes
channelsStateFlow
    .filterNotNull()
    .collectLatest { state ->
        // Update your UI with the new state
    }
```

The `QueryChannelsState` exposes multiple observable properties such as:

- `channelsStateData`: A `StateFlow` containing the current state of the query. Can be one of the following:
  - `ChannelsStateData.NoQueryActive`: No query has been executed yet.
  - `ChannelsStateData.Loading`: The query is currently loading.
  - `ChannelsStateData.OfflineNoResults`: No connectivity and no offline results are available.
  - `ChannelsStateData.Result`: A successful result containing the list of channels matching the query.
- `nextPageRequest`: A `StateFlow` containing the next page request if more channels are available for pagination.
- `loadingMore`: A `StateFlow` indicating whether more channels are currently being loaded.
- `endOfChannels`: A `StateFlow` indicating whether the end of the channel list has been reached.

You can see all available properties in [QueryChannelsState](https://github.com/GetStream/stream-chat-android/blob/v6/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/querychannels/QueryChannelsState.kt).

#### Pagination

A common use case when querying channels is to load more channels as the user scrolls. You can use the `nextPageRequest` property to get the next page request, and then call `ChatClient.queryChannels` using the provided request to load more channels. The newly loaded channels will be automatically reflected in the already observed `QueryChannelsState` object.

```kotlin
val nextPageRequest = channelsStateFlow.value?.nextPageRequest?.value ?: return
chatClient.queryChannels(nextPageRequest).enqueue()
```

For a detailed example of querying channels and displaying them in a Compose UI, see [Custom Channel List](/chat/docs/sdk/android/v6/compose-cookbook/custom-channel-list/).

#### Custom event handling

The `queryChannelsAsState` also accepts an optional `ChatEventHandlerFactory` parameter. This allows you to introduce custom handling of chat events which would affect the channel list state. This is usually not needed, as the default event handling covers most use cases. A common use case for this is when you want to observe two or more different channel lists at the same time—you can create an event handler factory that scopes the events to only the relevant channel list. For more details, see [Channels State and Filtering](/chat/docs/sdk/android/v6/client/guides/channels/).

### Watch channel

To get the data for a specific channel (including messages, members and watchers), while also receiving real-time updates, you can use the `ChatClient.watchChannelAsState` extension function. This function accepts the ID of the channel to watch, and a message limit for the initial load. It returns a `StateFlow` of `ChannelState` that you can collect to observe changes to the channel state.

```kotlin
val cid = "messaging:123"
val limit = 30

// Obtain the StateFlow for the channel state
val channelStateFlow: StateFlow<ChannelState?> =
    chatClient.watchChannelAsState(cid, limit)

// Collect the state flow to observe changes
channelStateFlow
    .filterNotNull()
    .collectLatest { state ->
        // Update your UI with the new channel state
    }
```

The `ChannelState` exposes multiple observable properties such as:

- `messagesState`: A `StateFlow` containing the current state of messages in the channel. Can be one of the following:
  - `MessagesState.NoQueryActive`: No query is currently running.
  - `MessagesState.Loading`: Messages are currently being loaded.
  - `MessagesState.OfflineNoResults`: No connectivity and no offline messages are available.
  - `MessagesState.Result`: A successful result containing the list of currently loaded messages in the channel.
- `messages`: A `StateFlow` containing the raw list of currently loaded messages in the channel.
- `loadingOlderMessages`: A `StateFlow` indicating whether older messages are currently being loaded.
- `loadingNewerMessages`: A `StateFlow` indicating whether newer messages are currently being loaded.
- `endOfOlderMessages`: A `StateFlow` indicating whether the beginning of the message list has been reached.
- `endOfNewerMessages`: A `StateFlow` indicating whether the end of the message list has been reached.
- `pinnedMessages`: A `StateFlow` containing the list of currently loaded pinned messages in the channel.
- `members`: A `StateFlow` containing the list of currently loaded members in the channel.
- `watchers`: A `StateFlow` containing the list of currently loaded watchers in the channel.
- `unreadCount`: A `StateFlow` containing the unread message count for the current user in the channel.

You can see all available properties in [ChannelState](https://github.com/GetStream/stream-chat-android/blob/v6/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/state/ChannelState.kt).

#### Pagination

To handle the pagination of messages, the `StatePlugin` exposes several helper methods:

- `ChatClient.loadOlderMessages(cid: String, messageLimit: Int)`: Loads older messages for the specified channel. It will load messages older than the currently loaded oldest message.
- `ChatClient.loadNewerMessages(cid: String, baseMessageId: String, messageLimit: Int)`: Loads newer messages for the specified channel. It will load messages newer than the provided base message ID.
- `ChatClient.loadMessagesAroundId(cid: String, messageId: String)`: Loads messages around a specific message ID for the specified channel. Useful for jumping to a specific message in the channel (ex. jumping to a pinned message or a quoted message).

For a detailed example of watching a channel and displaying its messages in a Compose UI, see [Custom Message List](/chat/docs/sdk/android/v6/compose-cookbook/custom-message-list/).

### Query threads

To get a list of threads of which the current user is a participant, while also receiving real-time updates, you can use the `ChatClient.queryThreadsAsState` extension function. This function accepts a `QueryThreadsRequest` specifying the query criteria, and it returns a `StateFlow` of `QueryThreadsState`. You can collect this state flow to observe changes to the list of threads.

```kotlin
// Define the query request
val request = QueryThreadsRequest(
    filter = Filters.eq("channel_cid", "messaging:123"),
    limit = 10,
)

// Obtain the StateFlow for the query threads state
val threadsStateFlow: StateFlow<QueryThreadsState?> =
    chatClient.queryThreadsAsState(request)

// Collect the state flow to observe changes
threadsStateFlow
    .filterNotNull()
    .collectLatest { state ->
        // Update your UI with the new state
    }
```

The `QueryThreadsState` exposes multiple observable properties such as:

- `threads`: A `StateFlow` containing the list of currently loaded thread messages.
- `loading`: A `StateFlow` indicating whether the threads are currently being loaded.
- `loadingMore`: A `StateFlow` indicating whether more threads are currently being loaded.
- `next:` A `StateFlow` containing the next page cursor if more threads are available for pagination.
- `unseenThreadIds`: A `StateFlow` containing the list of thread IDs that are not yet loaded, but have new messages since the current thread list was loaded.

You can see all available properties in [QueryThreadsState](https://github.com/GetStream/stream-chat-android/blob/v6/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/querythreads/QueryThreadsState.kt).

#### Pagination

A common use case when querying threads is to load more threads as the user scrolls. You can use the `next` property to get the next page cursor, and then call `ChatClient.queryThreadsResult` using the provided cursor to load more threads. The newly loaded threads will be automatically reflected in the already observed `QueryThreadsState` object.

```kotlin
val initialRequest = QueryThreadsRequest()
val next = threadsStateFlow.value?.next?.value ?: return
val nextRequest = initialRequest.copy(next = next)
chatClient.queryThreads(nextRequest).enqueue()
```

### Thread replies

To watch the replies of a specific message, while also receiving real-time updates, you can use the `ChatClient.getRepliesAsState` suspend extension function. This function accepts the ID of the thread (ID of the thread parent message), and a message limit for the initial load. It returns a `ThreadState` object that exposes multiple observable properties.

```kotlin
val threadId = "message-123"
val limit = 30

// Obtain the ThreadState for the thread (must be called from a coroutine)
val threadState: ThreadState =
    chatClient.getRepliesAsState(threadId, limit, olderToNewer = false)

// Collect the messages StateFlow to observe changes
threadState
    .messages
    .collectLatest { messages ->
        // Update your UI with the new list of replies
    }
```

The `ThreadState` exposes multiple observable properties such as:

- `messages`: A `StateFlow` containing the list of currently loaded replies in the thread.
- `loading`: A `StateFlow` indicating whether replies are currently being loaded.
- `endOfOlderMessages`: A `StateFlow` indicating whether the beginning of the reply list has been reached.
- `endOfNewerMessages`: A `StateFlow` indicating whether the end of the reply list has been reached.

You can see all available properties in [ThreadState](https://github.com/GetStream/stream-chat-android/blob/v6/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/channel/thread/ThreadState.kt).

#### Pagination

To handle the pagination of replies, you can use the `ChatClient.getRepliesMore(messageId: String, firstId: String, limit: Int)` method. It accepts the thread ID (ID of the thread parent message), the ID of the message to use as a base for loading more replies, and the number of replies to load. The newly loaded replies will be automatically reflected in the already observed `ThreadState` object.

```kotlin
val firstId = threadState.oldestInThread.value?.id ?: return
chatClient.getRepliesMore(threadId, firstId, limit = 20).enqueue()
```

### Global state

The `StatePlugin` also exposes some globally accessible state properties for the currently logged in user, which are not directly linked to a specific channel or thread. These properties are exposed via the `GlobalState` object, which can be accessed using the `ChatClient.globalStateFlow` property:

```kotlin
// Obtain the Flow for the global state
val globalStateFlow: Flow<GlobalState> = chatClient.globalStateFlow

// Collect the global state flow to observe changes
globalStateFlow
    .collectLatest { state ->
        // Update your UI with the new global state
    }
```

The `GlobalState` exposes multiple observable properties such as:

- `totalUnreadCount`: A `StateFlow` containing the total unread message count across all channels for the current user.
- `channelUnreadCount`: A `StateFlow` containing the number of unread channels for the current user.
- `unreadThreadsCount`: A `StateFlow` containing the number of unread threads for the current user.
- `muted`: A `StateFlow` containing the list of currently muted users for the current user.
- `channelMutes`: A `StateFlow` containing the list of currently muted channels for the current user.
- `blockedUserIds`: A `StateFlow` containing the list of user IDs that are currently blocked by the current user.
- `activeLiveLocations`: A `StateFlow` containing the list of active live locations that are being shared in the app.
- `currentUserActiveLiveLocations`: A `StateFlow` containing the list of active live locations that are being shared in the app by the current user.

You can see all available properties in [GlobalState](https://github.com/GetStream/stream-chat-android/blob/v6/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/global/GlobalState.kt).

<admonition type="info">
The <b>ChatClient.globalStateFlow</b> will not emit any values until a user is connected.
</admonition>

#### Unread counts

A common use case for the `GlobalState` is to observe the total unread channel count to update the app badge or show unread indicators in the UI. To do this, you can collect the `totalUnreadCount` state flow, emitted by the `globalStateFlow`:

```kotlin
chatClient.globalStateFlow
    .flatMapLatest { it.totalUnreadCount }
    .collectLatest { totalUnreadCount ->
        // Update your UI with the new channel unread count
    }
```

Similarly, you can observe other global state properties as needed.


---

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

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