Native Jetpack Compose chat UI for Chatty — zero WebView, zero compromise.
Drop a fully native, on-brand support chat into any Android app in minutes. Talks directly to
the same /api/widget/* backend as the Chatty web widget, and renders every bubble, avatar, and
composer with real Compose UI — fast, themeable, and indistinguishable from the rest of your app.
Install · Quick start · Design gallery · API reference · Example app
| No WebView, anywhere | Every bubble, avatar, and the composer are real @Composables — no iframe, no JS bridge, no WebView memory overhead. |
| Matches your dashboard automatically | Fetches the bot's theme and renders with the exact colors, corner radii, and launcher shape chosen in the dashboard — no manual styling. |
| Two integration shapes | A floating ChattyLauncher bubble + dialog, or an embedded ChattyChatScreen inside your own layout. |
| A real composer, not a stub | Full-Unicode emoji picker (search + categories + skin tones, via androidx.emoji2), animated attach menu (camera, gallery, documents, location), and mic-to-text voice notes — built in, not bolted on. |
| Small dependency footprint | OkHttp, Coil, and Jetpack Compose Material3. Nothing else. |
Note
Use v1.2.0 or later. v1.0.0–v1.0.2 predate fixes that were needed for the SDK, the
example app, and CI to actually build cleanly. Full history in the
releases — every tag from
v1.0.3 onward is CI-verified green before it ships.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}// app/build.gradle.kts
dependencies {
implementation("com.github.PersonaliAI:chatty-android-sdk:v1.2.0")
}Via Maven Central (published, but currently lagging behind JitPack)
Live at com.personaliai:chatty-android-sdk, but stuck at 1.0.0 — later releases haven't been
pushed through release.yml yet. Use the JitPack coordinate above for the latest fixes until this
catches up.
dependencies {
implementation("com.personaliai:chatty-android-sdk:1.0.0")
}As a local module (building from source)
// settings.gradle.kts
include(":chatty-sdk")
project(":chatty-sdk").projectDir = file("../chatty-android-sdk/chatty-sdk")// app/build.gradle.kts
dependencies {
implementation(project(":chatty-sdk"))
}Find your bot ID in the Chatty dashboard under Embed & Integrate → Android SDK.
Floating launcher (recommended) — a bubble that expands into a full-screen chat dialog, the native equivalent of the web widget's launcher button:
@Composable
fun AppRoot() {
Box(Modifier.fillMaxSize()) {
// ...your app content...
ChattyLauncher(botId = "YOUR_BOT_ID")
}
}Embedded full-screen chat — place it directly in your own navigation, e.g. as a "Support" tab:
@Composable
fun SupportScreen() {
ChattyChatScreen(botId = "YOUR_BOT_ID", modifier = Modifier.fillMaxSize())
}The SDK ships all 10 Chatty widget designs as Compose color/radius tokens, ported 1:1 from the
web widget's globals.css, so a native screen looks like whatever design is chosen in the
dashboard rather than one generic look. No configuration required — the SDK fetches the bot's
theme and resolves the matching token set automatically, including legacy widget_style IDs
from older presets.
| Design | Accent |
|---|---|
minimal |
|
playful |
|
corporate |
|
dark-sleek |
|
gradient-glow |
|
glassmorphism |
|
ecommerce |
|
healthcare-calm |
|
neubrutalism |
|
luxury-editorial |
Font pairing (each web design uses a distinct Google Font) is intentionally out of scope for this release; color, radius, and header/bubble treatment carry most of a design's identity.
@Composable
fun ChattyLauncher(
botId: String,
baseUrl: String = CHATTY_DEFAULT_BASE_URL,
host: String? = null,
position: ChattyPosition = ChattyPosition.BOTTOM_END,
color: Color? = null,
onVoiceCallPress: (() -> Unit)? = null,
onNotificationBellPress: (() -> Unit)? = null,
enableVoiceNotes: Boolean = true,
enableNotificationBell: Boolean = true,
enableLocationSharing: Boolean = true,
)| Param | Description |
|---|---|
botId |
Required. Your bot's ID from the dashboard. |
baseUrl |
Chatty backend base URL. Defaults to the production API. |
host |
Advisory only — sent to the backend but not used for access control. See Notes. |
position |
Corner the bubble docks to. Default BOTTOM_END. |
color |
Overrides the launcher color. Defaults to the active design's accent color. |
onVoiceCallPress |
Forwarded to ChattyChatScreen's header voice-call button. See Notes. |
onNotificationBellPress |
Forwarded to ChattyChatScreen's header notification bell. See Notes. |
enableVoiceNotes |
Forwarded to ChattyChatScreen. See Permissions. |
enableNotificationBell |
Forwarded to ChattyChatScreen. See Permissions. |
enableLocationSharing |
Forwarded to ChattyChatScreen. See Permissions. |
@Composable
fun ChattyChatScreen(
botId: String,
baseUrl: String = CHATTY_DEFAULT_BASE_URL,
host: String? = null,
hostKey: String = "app",
modifier: Modifier = Modifier,
onMessage: ((ChattyMessage) -> Unit)? = null,
onVoiceCallPress: (() -> Unit)? = null,
onNotificationBellPress: (() -> Unit)? = null,
onClose: (() -> Unit)? = null,
enableVoiceNotes: Boolean = true,
enableNotificationBell: Boolean = true,
enableLocationSharing: Boolean = true,
)| Param | Description |
|---|---|
botId |
Required. Your bot's ID from the dashboard. |
baseUrl |
Chatty backend base URL. Defaults to the production API. |
host |
Advisory only — sent to the backend but not used for access control. See Notes. |
hostKey |
Storage key used to namespace the locally persisted conversation. |
modifier |
Standard Compose Modifier for sizing/placement. |
onMessage |
Called for every inbound message — useful for unread badges or analytics. |
onVoiceCallPress |
Header voice-call button tapped. Only shown when the bot's dashboard has voice enabled. See Notes. |
onNotificationBellPress |
Header notification-bell button tapped (only shown once POST_NOTIFICATIONS is already granted). See Notes. |
onClose |
Renders a close (✕) button in the header when set. ChattyLauncher passes this for you; set it yourself only if you're embedding ChattyChatScreen directly inside your own dialog/sheet. |
enableVoiceNotes |
Default true. Gates the composer's mic button on RECORD_AUDIO already being granted — set false to hide it regardless of permission state. See Permissions. |
enableNotificationBell |
Default true. Gates the header's bell button on POST_NOTIFICATIONS already being granted — set false to hide it regardless of permission state. See Permissions. |
enableLocationSharing |
Default true. Gates the attach menu's Location option on ACCESS_COARSE_LOCATION already being granted — set false to hide it regardless of permission state. See Permissions. |
Permissions — what this SDK declares, and how to opt out
The SDK's own AndroidManifest.xml declares three dangerous/runtime-gated permissions, all of
which get merged into your app's manifest automatically by the Android Gradle Plugin. Camera and
Photo Library/Documents aren't in this table — they need no permission at all (system camera
intent, Storage Access Framework pickers):
| Permission | Risk | Used for | Button shown when |
|---|---|---|---|
RECORD_AUDIO |
Dangerous | Composer mic button → voice-note transcription | Already granted, and enableVoiceNotes (default true) |
POST_NOTIFICATIONS |
Runtime-gated (Android 13+) | Header bell button → onNotificationBellPress |
Already granted (or pre-13, where it's implicit), and enableNotificationBell (default true) |
ACCESS_COARSE_LOCATION |
Dangerous | Attach menu's Location option → drops a Google Maps link into the composer text | Already granted, and enableLocationSharing (default true) |
This SDK never calls the OS permission dialog itself, for any of these three. Each button/menu
option above simply doesn't render until its permission is already granted — no silently-failing
button, and no system dialog popping up from code your app doesn't own the timing of. Your app is
responsible for actually requesting RECORD_AUDIO/POST_NOTIFICATIONS/ACCESS_COARSE_LOCATION
(its own rememberLauncherForActivityResult(RequestPermission()), wherever and whenever fits your
own onboarding/permission-priming flow); once granted, the corresponding button appears
automatically (re-checked whenever the screen resumes — including right after your app's own
permission dialog resolves). Set the matching enable* param to false if you don't want the
button to ever appear, independent of permission state.
If you also need the permission gone from your app's own merged manifest (e.g. for a Play Store
Data Safety form, or a security review that flags any declared dangerous permission), add an
explicit removal to your app's AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
</manifest>Do this only alongside the matching enable* param set to false (e.g. enableVoiceNotes = false for RECORD_AUDIO) — removing a manifest permission while still showing its button leaves
a dead button whose permission request will always fail.
The voice-call feature (ChattyVoiceCallScreen, opt-in, requires LiveKit) needs its own
RECORD_AUDIO request — see Voice-call button below; it's independent of the composer
mic button and isn't controlled by enableVoiceNotes.
Security — bot_id and domain restriction
bot_id is not a secret — it's extractable from any client, web or mobile. Domain restriction
(allowed_domains in the dashboard) is enforced by the backend as a rate-limit tier, not a
hard reject: verified web traffic gets 30 msgs/60s per bot+IP, everything else (including all
mobile SDK traffic — there's no way for a native app to obtain a "verified" token the way a
browser's Referer allows) gets throttled to 5 msgs/120s. The host param this SDK sends is
advisory only and isn't used for access control. If your bot is mobile-primary, leave
allowed_domains empty to get the normal 30/60s tier instead.
Notification bell — what it does and doesn't do
Only rendered once POST_NOTIFICATIONS is already granted (Android 13+ only — older versions
grant it at install time, so the bell always shows there); tapping it just calls
onNotificationBellPress — the SDK never requests the permission itself, see
Permissions. That's as far as this SDK goes.
Actually delivering a push when a reply arrives while the app is backgrounded needs a push
provider wired up at the app level — either Firebase Cloud Messaging directly (free, no third
party) or a wrapper like OneSignal (adds a dashboard/API for managing sends, at the cost of
another vendor). Either way it's the same shape of work: register the device's push token, send
it to your own backend, store it against the session/user, and have your backend call
FCM/OneSignal's send API when a new assistant/agent message lands for a session that isn't
actively polling. None of that exists yet — it's backend work in chatty-backend, not something
this client SDK can add on its own.
Voice-call button
Only shown when the bot's dashboard has voice enabled, and fires onVoiceCallPress. This SDK now
ships a ready-to-render call screen, ChattyVoiceCallScreen — render it yourself from that
callback (it's opt-in: only apps that use it need LiveKit's Android SDK pulled in):
// build.gradle.kts (your app module)
dependencies {
implementation("io.livekit:livekit-android:2.18.2") // or newer
}// Application.onCreate(), once:
LiveKit.init(applicationContext)var showCall by remember { mutableStateOf(false) }
if (showCall) {
ChattyVoiceCallScreen(
client = client,
sessionId = sessionId, // the same session id ChattyChatScreen/ChattyViewModel is using
widgetStyle = state.theme?.widgetStyle,
onClose = { showCall = false },
)
} else {
ChattyChatScreen(state = state, onVoiceCallPress = { showCall = true }, /* ... */)
}Also add <uses-permission android:name="android.permission.RECORD_AUDIO" /> to your
AndroidManifest.xml and request it at runtime before the call screen is shown.
- Lead capture and meeting booking happen conversationally (the assistant decides to ask/act) — there's no separate REST call to trigger them from the SDK.
- Polling for human-agent takeover messages runs every 4s while
ChattyChatScreenis composed, matching the web widget's behavior. - Conversation history is persisted locally (
SharedPreferences), mirroring the web widget'slocalStoragecache, so a returning user sees their prior messages.
chatty-example-app/ is a minimal, runnable Compose app demonstrating both
integration styles side by side — open it in Android Studio, hit run, and try the floating
launcher and the embedded full-screen chat against a live demo bot. It's its own Gradle root
(built by Android Studio's project wizard, so it starts from current, known-compatible
Gradle/AGP/JDK defaults) that pulls in :chatty-sdk as a sibling-directory source dependency —
open chatty-example-app/, not the repo root, in Android Studio.
cd chatty-example-app && ./gradlew :app:installDebug-
minSdk 24+ -
Kotlin, Jetpack Compose (Material3)
-
OkHttp, Coil (image loading) — pulled in automatically as transitive dependencies
-
Core library desugaring enabled in your app module — the SDK uses
java.timeAPIs desugared down tominSdk 24, and the Android Gradle Plugin enforces that any consumer of an AAR built this way opts in too:// app/build.gradle.kts android { compileOptions { isCoreLibraryDesugaringEnabled = true } } dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") }
Contributing — bug reports, design-parity fixes, and PRs are welcome.
Licensed under MIT © PersonaliAI