class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val client = ChatClient.Builder(apiKey, this)
.withPlugins(
StreamOfflinePluginFactory(appContext = this),
StreamStatePluginFactory(config = StatePluginConfig(), appContext = this),
)
.build()
}
}Troubleshooting
Common issues and solutions when integrating the Stream Chat Android SDK.
Initialization Issues
ChatClient Not Initialized
Symptom: IllegalStateException: ChatClient is not initialized
Cause: Attempting to use ChatClient.instance() before calling ChatClient.Builder(...).build().
Solution: Initialize ChatClient in your Application class:
ProGuard/R8 Issues
Symptom: Crashes in release builds, missing classes, or serialization errors.
Cause: ProGuard/R8 obfuscation removing required classes.
Solution: The SDK includes consumer ProGuard rules automatically. See the consumer rules in the SDK repository for details. If you still encounter issues, ensure the consumer rules are being applied correctly by your build configuration.
Push Notification Issues
FCM Token Not Refreshing
Symptom: Push stops working after token refresh.
Solution: Use FirebaseMessagingDelegate to handle token registration and refresh:
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
FirebaseMessagingDelegate.registerFirebaseToken(
token = token,
providerName = "MyFirebaseProvider", // Must match dashboard config
)
}
override fun onMessageReceived(message: RemoteMessage) {
FirebaseMessagingDelegate.handleRemoteMessage(message)
}
}The snippet above (using FirebaseMessagingDelegate) is the recommended way to handle token refresh. If you have configured FirebasePushDeviceGenerator in your NotificationConfig, the SDK already registers the device automatically on connectUser — do not also call ChatClient.addDevice(...) from your application code.
If your application does not use FirebasePushDeviceGenerator and you manage the Firebase token flow yourself, you can register the device manually by calling ChatClient.addDevice(...) from your own FirebaseMessagingService.onNewToken(...). The call requires a connected user, so ensure ChatClient.connectUser(...) has succeeded first. If onNewToken fires before a user is connected, cache the token and register it after the next successful connectUser(...):
ChatClient.instance().addDevice(
Device(
token = token,
pushProvider = PushProvider.FIREBASE,
providerName = "MyFirebaseProvider",
)
).enqueue { /* Handle result */ }State & UI Issues
Messages Not Updating
Symptom: New messages don't appear or UI doesn't reflect changes.
Cause: The channel is not being watched. Messages only update in real-time for watched channels.
Solution: Ensure the channel is watched before expecting real-time updates:
// Watch the channel to receive real-time updates
ChatClient.instance().channel(channelType, channelId).watch().enqueue { result ->
when (result) {
is Result.Success -> { /* Channel is now watched */ }
is Result.Failure -> { /* Handle error */ }
}
}For Compose, use the provided ViewModels which handle watching automatically:
val viewModelFactory = MessagesViewModelFactory(
context = context,
channelId = cid,
)
val messageListViewModel = viewModel<MessageListViewModel>(factory = viewModelFactory)Channel List Not Refreshing
Symptom: Channel list shows stale data or doesn't update with new messages.
Cause: Event handling not configured or custom filter issues.
Solution:
- Verify you're using the SDK's ViewModel or observing state correctly
- Check your
ChatEventHandlerif using custom event handling (see Channels State and Filtering) - Ensure the
StatePluginis configured
Compose Recomposition Issues
Symptom: Compose UI updates excessively or not at all.
Solution: Use the SDK's state hoisting pattern:
// Let the ViewModel handle state
val messagesState by messageListViewModel.currentMessagesState.collectAsStateWithLifecycle()
MessageList(
currentState = messagesState,
// ...
)Avoid creating new objects in composition that would trigger recomposition.
Connection Issues
Frequent Disconnections
Symptom: WebSocket disconnects frequently, especially on mobile networks.
Cause: Network changes or aggressive keep-alive settings.
Solution: Handle connectivity changes:
ChatClient.instance().clientState.connectionState
.onEach { state ->
when (state) {
is ConnectionState.Connected -> { /* Online */ }
is ConnectionState.Connecting -> { /* Reconnecting */ }
is ConnectionState.Offline -> { /* Handle offline */ }
}
}
.launchIn(scope)Token Expiry
Symptom: 401 Unauthorized errors or connection failures after some time.
Cause: JWT token expired and tokenProvider not configured.
Solution: Provide a token provider that fetches fresh tokens:
ChatClient.Builder(apiKey, context)
.withPlugins(/* ... */)
.build()
// Connect with token provider
ChatClient.instance().connectUser(
user = user,
tokenProvider = {
// Fetch fresh token from your backend
myBackend.getStreamToken(userId)
}
).enqueue { result ->
// Handle result
}For more details on handling token expiry with push notifications, see the Push Notifications documentation.
UnknownHostException From the Token Provider in the Background
Symptom: A high volume of non-fatal UnknownHostException (for example "Unable to resolve host") coming from your TokenProvider.loadToken(), almost always while the app is backgrounded.
Cause: loadToken() is called on the initial connection and again when a fresh token is needed (a token expiry or authentication error, an unhealthy connection that is re-established, an unrecoverable socket error, or an explicit disconnect() / disconnectSocket()). A reconnection that needs a fresh token can run while the app is in the background, and on a poor or restricted network (Doze, Wi-Fi sleep, network handoff, or App Standby buckets) the DNS lookup for your backend fails. The SDK treats this as a failed connection attempt and retries, so it is not a crash.
There are two call sites:
- The SDK's own reconnection:
SocketFactory.buildUrl→TokenManager.ensureTokenLoaded→loadToken. - Your app calling
connectUser():ChatClient.connectUser→setUser→loadToken. The SDK never calls the publicconnectUser()for you, so this path only runs when your code does.
Solution:
- Make your
TokenProviderresilient to a missing network. When there is no connectivity, fail fast and do not record the expected offline failure as a non-fatal in your crash reporter. - Gate your own
connectUser()calls behind a foreground and connectivity check, so you do not start a connection while backgrounded. - You usually do not need to disconnect manually on background. The SDK already stops the socket when the app goes to the background and reconnects when it returns to the foreground. See Handling User Connection for the full lifecycle behavior.
Memory & Performance
Message List Performance
Symptom: Laggy scrolling in channels with many messages.
Solution:
- Use
MessageLimitConfigto limit in-memory messages - For Compose, the
MessageListusesLazyColumnwhich virtualizes automatically - For XML Views, ensure you're not blocking the main thread in custom view holders
Debugging Tips
Enable Logging
Enable detailed logging to diagnose issues:
ChatClient.Builder(apiKey, context)
.logLevel(ChatLogLevel.ALL)
.loggerHandler(AndroidStreamLogger())
.build()Filter logcat by tag Chat: to see SDK logs.
Inspect Network Requests
Use the ApiRequestsAnalyser to track API calls:
val analyser = ApiRequestsAnalyser.get()
analyser.dumpRequestByName("QueryChannels")Check Connection State
Monitor connection state for debugging:
ChatClient.instance().clientState.connectionState.value
ChatClient.instance().clientState.user.value
ChatClient.instance().clientState.initializationState.value