Skip to content

SDKs

Kotlin

On this page

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.current outside Notifiable.
  • missing_token — reserved when a push port disallows empty tokens; the FCM port allows missing tokens and saves not_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

  1. Depend on the notifiable Android library. Import sh.notifiable. Do not depend on the internal core module from app code.
  2. Call Notifiable(...) in Compose or Notifiable.attach(...) / NotifiableClient(...) from onCreate before the activity starts.
  3. Pass publicKey, optional deepLinks, and onDeepLink that navigates with the app’s NavController / router.
  4. Wait for snapshot.ready before identify / inbox / track APIs. Call requestPermission() from a button.
  5. After sign-in, call identify with email and/or externalId.
  6. Firebase / google-services.json only when an FCM token is required; connect still works without it.
  7. 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.