Production-ready Android chat SDK built with Kotlin + Jetpack Compose.
This package ships a complete chat experience (room list + room view + media + reactions + typing + push hooks) and exposes host-facing APIs for configuration, connection state, unread counters, event interception, and UI extension.
- What You Get
- Architecture
- Requirements
- Installation
- Host App Setup
- Quick Start
- Authentication Modes
- Single Room vs Multi Room
- Public APIs for Host UI
- ChatConfig Reference
- Event and Send Interception
- Custom UI Components
- Push Notifications (FCM)
- Persistence and Offline Behavior
- Logout
- Troubleshooting
- Production Checklist
- Compose chat UI with room list and room screen.
- Real-time messaging over XMPP WebSocket.
- History loading + incremental sync after reconnect.
- Unread counters and host-facing connection status hook.
- Media messages (image/video/audio/files), full-screen image viewer, PDF/web preview support.
- Message actions: edit, delete, reply, reactions.
- Typing indicators.
- URL auto-linking + URL preview cards.
- Push integration hooks (FCM token/backend subscription + room MUC-SUB flow).
- Local persistence for user/session metadata, rooms, message cache, and scroll position.
- Extensibility hooks: event stream, send interception, custom composables.
This repository contains:
ethora-component: distributable SDK artifact (published to JitPack).chat-core: networking, XMPP, stores, models, persistence, push manager.chat-ui: Compose UI + hooks (Chat,useUnread,useConnectionState,reconnectChat).sample-chat-app: reference app with full integration.
Important: the published artifact is ethora-component, but it packages code from chat-core and chat-ui via source sets.
- Android
minSdk 26 compileSdk 34,targetSdk 34- Java/Kotlin target
17 - Kotlin
2.3.0, AGP8.7.0, Gradle9.4.1 - Host must apply
id("org.jetbrains.kotlin.plugin.compose")(required since Kotlin 2.0 —composeOptions.kotlinCompilerExtensionVersionis no longer used) - Jetpack Compose app (or host screen using Compose)
- Network permissions in host manifest
Host AndroidManifest.xml minimum:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />If using push on Android 13+:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />- Add JitPack repository in project
settings.gradle.kts:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}- Add dependency in app module:
dependencies {
implementation("com.github.dappros.ethora-sdk-android:ethora-component:<version>")
}Use a release tag or commit SHA for <version>.
Copy these folders into your project root:
ethora-componentchat-corechat-ui
Then:
// settings.gradle.kts
include(":ethora-component")// app/build.gradle.kts
dependencies {
implementation(project(":ethora-component"))
}Before rendering Chat(...), initialize SDK stores (same pattern as sample app):
import android.content.Context
import com.ethora.chat.core.persistence.ChatDatabase
import com.ethora.chat.core.persistence.ChatPersistenceManager
import com.ethora.chat.core.persistence.LocalStorage
import com.ethora.chat.core.persistence.MessageCache
import com.ethora.chat.core.push.PushNotificationManager
import com.ethora.chat.core.store.MessageLoader
import com.ethora.chat.core.store.MessageStore
import com.ethora.chat.core.store.RoomStore
import com.ethora.chat.core.store.ScrollPositionStore
import com.ethora.chat.core.store.UserStore
fun initEthoraSdk(context: Context) {
val appContext = context.applicationContext
val persistenceManager = ChatPersistenceManager(appContext)
val chatDatabase = ChatDatabase.getDatabase(appContext)
val messageCache = MessageCache(chatDatabase)
RoomStore.initialize(persistenceManager)
UserStore.initialize(persistenceManager)
MessageStore.initialize(messageCache)
ScrollPositionStore.initialize(appContext)
MessageLoader.initialize(LocalStorage(appContext))
// Optional but recommended if push is enabled
PushNotificationManager.initialize(appContext)
}import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.fillMaxSize
import com.ethora.chat.Chat
import com.ethora.chat.core.config.ChatConfig
import com.ethora.chat.core.config.ChatHeaderSettingsConfig
import com.ethora.chat.core.config.JWTLoginConfig
import com.ethora.chat.core.config.XMPPSettings
@Composable
fun ChatScreen() {
val config = ChatConfig(
appId = "YOUR_APP_ID",
baseUrl = "https://api.your-domain.com/v1",
customAppToken = "JWT <YOUR_APP_TOKEN>",
disableRooms = false,
chatHeaderSettings = ChatHeaderSettingsConfig(),
xmppSettings = XMPPSettings(
xmppServerUrl = "wss://xmpp.your-domain.com/ws",
host = "xmpp.your-domain.com",
conference = "conference.xmpp.your-domain.com"
),
jwtLogin = JWTLoginConfig(
token = "<USER_JWT>",
enabled = true
)
)
Chat(
config = config,
modifier = Modifier.fillMaxSize()
)
}Current Android Chat(...) init order:
userparam inChat(config, user = ...)config.userLogin(if enabled)config.jwtLogin(if enabled)- persisted JWT token
defaultLoginplaceholder (no built-in email/password UI in SDK)
ChatConfig(
jwtLogin = JWTLoginConfig(token = userJwt, enabled = true)
)ChatConfig(
userLogin = UserLoginConfig(enabled = true, user = user)
)ChatConfig(disableRooms = true)Then pass room JID:
Chat(config = config, roomJID = "roomname@conference.example.com")Behavior:
- SDK opens room directly.
- Header back button is hidden automatically in single-room flow.
ChatConfig(disableRooms = false)Room list is shown first, then chat room screen.
import com.ethora.chat.useUnread
val unread = useUnread(maxCount = 99)
// unread.totalCount: Int
// unread.displayCount: String (e.g. "99+")import com.ethora.chat.reconnectChat
import com.ethora.chat.useConnectionState
val connection = useConnectionState()
// connection.status: OFFLINE | CONNECTING | ONLINE | DEGRADED | ERROR
// connection.reason: String?
// connection.isRecovering: Boolean
reconnectChat()ChatConfig contains many fields for cross-platform parity. Not every field is fully wired in this Android package version.
appId,baseUrl,customAppTokenxmppSettings,dnsFallbackOverridesjwtLogin,userLogin,defaultLogindisableRooms,defaultRoomsdisableHeader,disableMediachatHeaderSettings.roomTitleOverrideschatHeaderSettings.chatInfoButtonDisabledchatHeaderSettings.backButtonDisabledcolors,bubleMessage,backgroundChatdisableProfilesInteractionseventHandlers,onChatEvent,onBeforeSendcustomComponentsinitBeforeLoad— whentrue, the SDK runs the web-parity bootstrap (user fetch → rooms fetch → XMPP connect → private-store sync → per-room history preload) souseUnread()reports real counts before theChatcomposable mounts. Drive it viaEthoraChatProvider(wrap your app root) orEthoraChatBootstrap.initializeAsync(context, config)fromApplication.onCreate.
These exist in model/API for parity, but Android behavior may be partial or no-op depending on release:
googleLogin,customLoginchatHeaderBurgerMenu,forceSetRoom,setRoomJidInPathdisableRoomMenu,disableRoomConfig,disableNewChatButtoncustomRooms,roomListStyles,chatRoomStylesheaderLogo,headerMenu,headerChatMenurefreshTokens,translatesdisableUserCount,clearStoreBeforeInit,disableSentLogic,newArch,qrUrlsecondarySendButton,enableRoomsRetry,chatHeaderAdditionalbotMessageAutoScroll,messageTextFilter,whitelistSystemMessage,customSystemMessagedisableTypingIndicator,customTypingIndicatorblockMessageSendingWhenProcessing,disableChatInfouseStoreConsoleEnabled,messageNotifications
If you need guaranteed support for one of these parity fields, validate against your target SDK tag/commit and test in your integration.
import com.ethora.chat.core.config.OutgoingSendInput
import com.ethora.chat.core.config.SendDecision
val config = ChatConfig(
onBeforeSend = { input: OutgoingSendInput ->
val blocked = input.text?.contains("forbidden-word", ignoreCase = true) == true
if (blocked) SendDecision.Cancel else SendDecision.Proceed(input)
}
)import com.ethora.chat.core.config.ChatEvent
val config = ChatConfig(
onChatEvent = { event ->
when (event) {
is ChatEvent.MessageSent -> { /* analytics */ }
is ChatEvent.MessageFailed -> { /* observability */ }
is ChatEvent.ConnectionChanged -> { /* host banner */ }
else -> Unit
}
}
)ChatEvent types include message sent/failed/edited/deleted, reaction, media upload result, connection state changes.
Provide custom composables through customComponents to override:
- message rendering
- message actions
- input area
- room list item
- scrollable area/day separator/new message label
See com.ethora.chat.core.models.CustomComponents for exact signatures.
SDK handles subscription flows, but host app must provide Firebase setup and token lifecycle.
- Add
google-services.jsonto your app module. - Apply
com.google.gms.google-servicesplugin in host app.
Create a service extending FirebaseMessagingService, then forward token and JID payload:
PushNotificationManager.setFcmToken(token)
PushNotificationManager.setPendingNotificationJid(jid)Call once at app start:
PushNotificationManager.initialize(context)Request POST_NOTIFICATIONS permission from host app.
Notes:
Chat(...)consumesPushNotificationManager.fcmTokenand subscribes backend/rooms when user + XMPP are ready.- Opening notification with
notification_jidallows SDK to navigate to the room when rooms are loaded.
- Rooms/user/tokens persisted via DataStore (
ChatPersistenceManager). - Messages persisted in Room DB (
chat_database, tablemessages) viaMessageStore+MessageCache. - Loader behavior:
- cache-first rooms/messages for fast startup
- API refresh for rooms
- initial XMPP history load per room
- incremental sync after reconnect
- DNS fallback map supported via
dnsFallbackOverridesfor emulator/network edge cases.
Use public service:
import com.ethora.chat.core.ChatService
ChatService.logout.performLogout()Optional callback:
import com.ethora.chat.core.service.LogoutService
LogoutService.setOnLogoutCallback {
// navigate to logged-out host screen
}- Ensure stores are initialized before rendering
Chat(...). - Ensure user is authenticated (
jwtLoginoruserLogin). - Ensure
baseUrl,appId, and token values are valid.
- Verify
xmppSettings(xmppServerUrl,host,conference). - Confirm websocket endpoint reachable from device/emulator.
- Use
dnsFallbackOverridesif DNS resolution fails in emulator.
google-services.jsonpackage name must match hostapplicationId.- Ensure FCM token is received and forwarded to
PushNotificationManager.setFcmToken. - Ensure notification permission is granted (Android 13+).
- Ensure user token and refresh token are valid.
- Ensure
customAppTokenis set for your backend app.
- Replace demo/default endpoints and app tokens.
- Always provide your own
customAppTokenandappId. - Do not rely on default token embedded in
AppConfigfor production. - Pin SDK dependency to a tag/commit you have tested.
- Add host analytics via
onChatEvent. - Add host moderation/compliance hooks via
onBeforeSend. - Validate push flow end-to-end on real devices.
If you need this README split into docs per audience (integration, config reference, push, migration), keep this as root overview and move deep dives into docs/.