---
title: "Kotlin"
description: "Install sh.notifiable and register an Android app with Compose or views."
canonical: https://docs.notifiable.sh/kotlin
source: https://docs.notifiable.sh/kotlin.md
---

# 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:

```kotlin
// settings.gradle.kts
includeBuild("../kotlin") {
    dependencySubstitution {
        substitute(module("sh.notifiable:notifiable")).using(project(":notifiable"))
    }
}
```

```kotlin
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.

```kotlin
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()`):

```kotlin
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):

```kotlin
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 = "ada@example.com", externalId = "user_123")
}
```

`snapshot` is a `StateFlow` — collect it from the main thread.

### Permission

```kotlin
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.

```kotlin
notifiable.identify(
    email = "ada@example.com",
    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

```bash
cd kotlin && ./gradlew :core:test
```

## For AI agents

Agents should read this page as Markdown: [`/kotlin.md`](/kotlin.md). Start from [`/llms.txt`](/llms.txt) or [`/ai-agents.md`](/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:

```text
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.
```
