SDKs
Kotlin

Notifiable registers an Android app with Notifiable: connect with a public key, ask for notification permission, identify the user, list their inbox, record opens and taps, and track events.
Import sh.notifiable. Requires Android 5.0 (API 21). The SDK owns FCM — you do not pass a push port.
Install
Until Maven Central publishing, include the build from this repository:
// settings.gradle.kts
includeBuild("../kotlin") {
dependencySubstitution {
substitute(module("sh.notifiable:notifiable")).using(project(":notifiable"))
}
}
dependencies {
implementation("sh.notifiable:notifiable:0.1.0")
}
The example app uses implementation(project(":notifiable")).
Add google-services.json and apply the Google Services plugin when you want an FCM token. If Firebase is not initialized, the token is empty and connect still saves the subscriber. A missing google-services.json does not crash startup.
Call Notifiable or Notifiable.attach from onCreate, before the activity is started, so the permission request can be registered.
Integrate
Jetpack Compose
Notifiable builds the client, puts it in LocalNotifiable, and calls start() when the composable enters.
import sh.notifiable.LocalNotifiable
import sh.notifiable.Notifiable
import sh.notifiable.DeepLink
Notifiable(
publicKey = "ntf_pub_...",
deepLinks = listOf(
DeepLink(path = "/orders", label = "Orders"),
DeepLink(path = "/inbox", label = "Inbox"),
),
onDeepLink = { url ->
// Open this route with your navigator.
},
) {
val notifiable = LocalNotifiable.current
val snapshot = notifiable.snapshot.collectAsState().value
Text(if (snapshot.ready) "Connected" else "Connecting…")
}
Views
attach adds a click-through overlay and returns the client (also calls start()):
val notifiable = Notifiable.attach(
this,
publicKey = "ntf_pub_...",
deepLinks = listOf(DeepLink(path = "/orders", label = "Orders")),
onDeepLink = { url -> /* navigate */ },
)
| Argument | Required | Description |
|---|---|---|
publicKey |
yes* | Key from the Notifiable dashboard. Development key while building; production key for the store build. |
deepLinks |
no | Routes Notifiable can link to: DeepLink(path, label). |
onDeepLink |
no | Called with a notification url (e.g. development toast tap). |
logger |
no | debug / info / warn / error callbacks. |
storage |
no | Custom store. Defaults to SharedPreferences. |
client |
no | Compose only. Existing client; ignores publicKey. |
*Required unless you pass client (Compose).
Or build the client yourself (pass a ComponentActivity when you can, so permission can be requested):
val notifiable = NotifiableClient(
context = this,
publicKey = "ntf_pub_...",
deepLinks = listOf(DeepLink(path = "/orders", label = "Orders")),
onDeepLink = { url -> /* navigate */ },
)
lifecycleScope.launch {
notifiable.start()
notifiable.requestPermission()
notifiable.identify(email = "[email protected]", externalId = "user_123")
}
snapshot is a StateFlow — collect it from the main thread.
Permission
Button(onClick = {
scope.launch { notifiable.requestPermission() }
}) {
Text("Enable notifications")
}
Or ask from the Notifiable dashboard — the next connect opens the system prompt. provider is fcm. On Android 13+, requestPermission() asks for POST_NOTIFICATIONS. A granted permission with an empty token is saved as not_determined.
NotifiableClient API
Wait until snapshot.value.ready is true before identify, inbox, notification, openNotification, clickDelivery, or track.
State (snapshot)
| Field | Meaning |
|---|---|
ready |
Connect finished and the subscriber was saved. |
permission |
Granted / Denied / NotDetermined |
subscriber |
Device/person record, or null until connect. |
clientConfig |
Dashboard app record (android.package_name, environment). |
deepLinks |
Saved routes. |
devIndicator / diagnostics |
Development mark and crash-sharing choices. |
error |
Last NotifiableException from start / permission / identify. |
Methods
start() — Connects. Notifiable and attach call it for you.
requestPermission() — System prompt, then save.
identify(email, externalId, firstName, lastName) — Attach a signed-in user. At least one field required; blank email/external id rejected.
notifiable.identify(
email = "[email protected]",
externalId = "user_123",
firstName = "Ada",
lastName = "Lovelace",
)
inbox(query) — One page of notifications. InboxQuery(page, opened) filters. Each SubscriberNotification includes id, title, body, url, imageUrl, data, opened, openedAt, createdAt, deliveryId, and related fields.
notification(id) / openNotification(id) / clickDelivery(deliveryId) — Load, mark opened, record tap.
track(name, properties) — Event for the connected subscriber. Property values: EventProperty.Text, Number, Bool, or Null.
setDevIndicator(enabled) / setDiagnostics(enabled) — Development mark and diagnostics. No-ops for a production key.
Development vs production
| Development key | Production key | |
|---|---|---|
| First launch | Sheet to show/hide the Notifiable mark | No sheet |
| On screen | Draggable mark (package name check) | Nothing overlaid |
| Notifications | Recent unread can appear as top toasts while started + granted | System displays FCM |
| Diagnostics | Failed SDK calls can be shared | Nothing sent |
There is no keyboard shortcut to show the mark — call setDevIndicator(enabled = true).
Errors
Failed calls throw NotifiableException. Codes: invalid_config, invalid_public_key, network, validation_failed, subscriber_not_found, subscriber_limit, rate_limited, missing_token, missing_storage, missing_provider, not_implemented.
missing_provider—LocalNotifiable.currentoutsideNotifiable.missing_token— reserved when a push port disallows empty tokens; the FCM port allows missing tokens and savesnot_determined.
Tests
cd kotlin && ./gradlew :core:test
For AI agents
Agents should read this page as Markdown: /kotlin.md. Start from /llms.txt or /ai-agents.md when the framework is unknown.
Rules
- Depend on the
notifiableAndroid library. Importsh.notifiable. Do not depend on the internalcoremodule from app code. - Call
Notifiable(...)in Compose orNotifiable.attach(...)/NotifiableClient(...)fromonCreatebefore the activity starts. - Pass
publicKey, optionaldeepLinks, andonDeepLinkthat navigates with the app’s NavController / router. - Wait for
snapshot.readybefore identify / inbox / track APIs. CallrequestPermission()from a button. - After sign-in, call
identifywithemailand/orexternalId. - Firebase /
google-services.jsononly when an FCM token is required; connect still works without it. - Do not invent hosts, env names, or CORS settings. Development key for local builds; production key for Play Store builds.
Integration prompt
Copy this into a coding agent when wiring the SDK into an existing Android / Kotlin app:
Integrate the Notifiable Kotlin Android library (sh.notifiable) into this Android project.
1. Add the notifiable dependency (includeBuild / project dependency from the kotlin/ folder of notifiable-sdks until Maven Central is available).
2. If the app should receive FCM tokens, ensure google-services.json and the Google Services plugin are set up. Do not crash if Firebase is missing.
3. From the main Activity onCreate (before start), either wrap the Compose UI in Notifiable(publicKey, deepLinks, onDeepLink) { ... } or call Notifiable.attach(...).
4. Pass publicKey from the project’s BuildConfig / secrets (development key for debug, production key for release).
5. Implement onDeepLink to navigate with this app’s NavController / Compose navigation.
6. After sign-in, when snapshot.ready is true, call identify(email = ..., externalId = ...).
7. Add an “Enable notifications” button that launches requestPermission() from a coroutine scope.
8. If there is an inbox screen, use inbox(), openNotification(id), and clickDelivery(deliveryId).
9. Collect snapshot as StateFlow on the main thread. Do not explain CORS, API hosts, or environments — only the public key. Keep changes minimal and match existing project patterns.
