implementation("io.getstream:stream-chat-android-state:<latest-version>")State Layer
The state layer is represented by the StatePlugin - a Plugin which manages the chat state used for rendering the UI components.
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:
To use the StatePlugin, you need to create an instance of StreamStatePluginFactory, and register it with the ChatClient during its initialization:
// 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()// 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();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.
- 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 thepresenceparameter 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 theStatePluginmethods such asChatClient.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
MessageLimitConfigandChannelMessageLimitfor 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.
// 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: AStateFlowcontaining 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: AStateFlowcontaining the next page request if more channels are available for pagination.loadingMore: AStateFlowindicating whether more channels are currently being loaded.endOfChannels: AStateFlowindicating whether the end of the channel list has been reached.
You can see all available properties in QueryChannelsState.
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.
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.
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 to handle the case where you want to observe two or more different channel lists at the same time. For this purpose, you can create an event handler factory that scopes the events to only the relevant channel list. For more details, see Channel List Updates
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.
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: AStateFlowcontaining 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: AStateFlowcontaining the raw list of currently loaded messages in the channel.loadingOlderMessages: AStateFlowindicating whether older messages are currently being loaded.loadingNewerMessages: AStateFlowindicating whether newer messages are currently being loaded.endOfOlderMessages: AStateFlowindicating whether the beginning of the message list has been reached.endOfNewerMessages: AStateFlowindicating whether the end of the message list has been reached.pinnedMessages: AStateFlowcontaining the list of currently loaded pinned messages in the channel.members: AStateFlowcontaining the list of currently loaded members in the channel.watchers: AStateFlowcontaining the list of currently loaded watchers in the channel.unreadCount: AStateFlowcontaining the unread message count for the current user in the channel.
You can see all available properties in ChannelState.
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.
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.
// 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: AStateFlowcontaining the list of currently loaded thread messages.loading: AStateFlowindicating whether the threads are currently being loaded.loadingMore: AStateFlowindicating whether more threads are currently being loaded.next:AStateFlowcontaining the next page cursor if more threads are available for pagination.unseenThreadIds: AStateFlowcontaining 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.
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.
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 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.
val threadId = "message-123"
val limit = 30
// Obtain the ThreadState for the thread
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: AStateFlowcontaining the list of currently loaded replies in the thread.loadingOlderMessages: AStateFlowindicating whether older replies are currently being loaded.loadingNewerMessages: AStateFlowindicating whether newer replies are currently being loaded.endOfOlderMessages: AStateFlowindicating whether the beginning of the reply list has been reached.endOfNewerMessages: AStateFlowindicating whether the end of the reply list has been reached.
You can see all available properties in ThreadState.
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.
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:
// Obtain the StateFlow for the global state
val globalStateFlow: StateFlow<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: AStateFlowcontaining the total unread message count across all channels for the current user.channelUnreadCount: AStateFlowcontaining the number of unread channels for the current user.unreadThreadsCount: AStateFlowcontaining the number of unread threads for the current user.muted: AStateFlowcontaining the list of currently muted users for the current user.channelMutes: AStateFlowcontaining the list of currently muted channels for the current user.blockedUserIds: AStateFlowcontaining the list of user IDs that are currently blocked by the current user.activeLiveLocations: AStateFlowcontaining the list of active live locations that are being shared in the app.currentUserActiveLiveLocations: AStateFlowcontaining 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.
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:
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.