SDKs · Kotlin
Android SDK
Native push for your Android app. Register with FCM, manage opt-out, display notification images and handle taps with your own app navigation.
On this page
Before you start
- A native Android app with minimum API 23. The SDK build uses compileSdk 36 and Java 17; the included sample targets API 36.
- A Firebase Android app matching your application ID, and a test device with Google Play services.
- The Android platform enabled in the Notirabbit dashboard, with your exact package name and matching FCM credentials.
- Your public app key from App Settings → Keys & IDs.
The Kotlin SDK has no Expo or React Native dependency. For an Expo project, use the Expo SDK guide.
1. Build and add the local Maven package
The SDK source is in the private notirabbit/notirabbit-android repository. Your GitHub account needs read access before cloning. The current 0.1.0-SNAPSHOT is built from source; it is not available from a hosted Maven registry.
git clone git@github.com:notirabbit/notirabbit-android.git
cd notirabbit-android
./gradlew :sdk:publishReleasePublicationToLocalTestRepositoryThis writes the AAR, POM and Gradle metadata under the SDK checkout’s build/maven directory. Merge this repository into your app’s settings, replacing the path with that generated directory:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("/absolute/path/to/notirabbit-android/build/maven")
content { includeGroup("com.notirabbit") }
}
}
}dependencies {
implementation("com.notirabbit:notirabbit-android:0.1.0-SNAPSHOT")
}Use the Maven package so Firebase, WorkManager and the SDK’s other dependencies resolve together. Copying only the AAR does not add those dependencies. Each developer or CI environment must build the source and provide the configured local repository path.
2. Configure Firebase and your Android platform
Follow Firebase’s Android setup to add the correct google-services.json to your app module and apply the Google Services Gradle plugin. Keep your existing default Firebase app if it is already configured.
In Notirabbit, configure Android with the same application ID and Firebase project. Upload the FCM service-account credential to the dashboard. The Firebase client configuration belongs in your Android app; the service-account private key and Sending API secret never do.
The SDK includes the Notirabbit API and event URLs. You only supply your public app key; no endpoint environment variables are required.
3. Initialize in your Application
Initialize once in Application.onCreate(), including background starts, after the default Firebase app is configured. Keep the SDK, messaging service and WorkManager in your app’s default process.
package com.example.app
import android.app.Application
import com.notirabbit.android.Configuration
import com.notirabbit.android.Notirabbit
class NotirabbitApplication : Application() {
override fun onCreate() {
super.onCreate()
Notirabbit.initialize(this, Configuration(
appKey = "YOUR_PUBLIC_APP_KEY",
smallIcon = R.drawable.ic_notification,
channelId = "notirabbit",
channelName = "Notifications",
onError = { error ->
// Handle error.reason and error.httpStatus in your app.
},
))
}
}Create a monochrome ic_notification drawable and set your Application class in the manifest. If your app already has an Application class, merge the initialization there and retain its existing manifest entry.
<application android:name=".NotirabbitApplication">
<!-- Keep your existing activities, services and other settings. -->
</application>Android controls notification permission and channel settings. The SDK cannot override a blocked channel. An optional onNotification callback reports notification rendering; use the tap handler below for navigation.
4. Choose one FCM service integration
The SDK does not automatically register a messaging service. Choose the option that matches your app.
No existing messaging service
Add the SDK’s service inside your manifest’s application element:
<service
android:name="com.notirabbit.android.NotirabbitMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>Existing messaging service
Keep your existing service and its non-exported manifest entry. Forward these callbacks there, retaining any handling for your other provider:
package com.example.app
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.notirabbit.android.Notirabbit
class AppMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
if (Notirabbit.handleMessage(message)) return
// Dispatch unrelated messages to your existing provider.
}
override fun onNewToken(token: String) {
Notirabbit.onNewToken(token)
// Forward token refresh to your existing provider too.
}
override fun onDeletedMessages() {
Notirabbit.refresh()
// Run your existing provider's recovery as needed.
}
}Use one app-owned FCM service. Do not also declare the provided service when forwarding from your own. Notirabbit expects its data-only FCM payloads; Firebase Console notification messages do not exercise the same delivery path.
5. Ask for permission and manage opt-out
Your app requests Android 13+ POST_NOTIFICATIONS permission from an opt-in action. The SDK declares the permission, but never opens the system prompt. Target API 33+ to control when that prompt appears.
Notirabbit.subscribe { result ->
// Update your UI from the durable local state or error.
}Use Notirabbit.unsubscribe() for explicit opt-out and Notirabbit.getSubscriptionState { result -> /* Update UI. */ } to read local state. Only a later subscribe() reverses explicit opt-out. Token refresh and app relaunch do not.
Initialization and Activity resume reconcile existing permission and token state. Call Notirabbit.refresh() after your own settings flow. On API 23–32, initialization can register immediately when notifications are already allowed.
Callbacks run on the main thread and confirm durable local state. pending=true means registration or revocation still needs synchronization; it is not an immediate server acknowledgment. WorkManager retries eligible network work.
6. Handle cold and warm notification taps
Call Notirabbit.handleIntent from the launch Activity’s onCreate, onNewIntent and onResume. Update the Activity’s intent in onNewIntent. Resume handling lets the app retry a failed durable enqueue.
The example below combines permission handling with those callbacks. Merge it into your existing launch Activity, connect your opt-in button to requestPushPermission(), and replace its destination allowlist and routing.
Complete Activity integration
package com.example.app
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import com.notirabbit.android.Notirabbit
import com.notirabbit.android.Notification
class NotirabbitActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep your existing screen setup here.
consumeNotification(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
consumeNotification(intent)
}
override fun onResume() {
super.onResume()
consumeNotification(intent)
}
// Call from your notification opt-in button, not on every launch.
fun requestPushPermission() {
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 42)
} else {
Notirabbit.subscribe { result ->
// Update your UI from the durable local state or error.
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != 42) return
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
Notirabbit.subscribe()
} else {
Notirabbit.refresh()
}
}
private fun consumeNotification(intent: Intent?) {
Notirabbit.handleIntent(intent) { notification ->
val uri = Uri.parse(notification.url)
if (Notification.isSafeDestination(notification.url) &&
uri.scheme == "https" && uri.host == "example.com" &&
uri.path?.startsWith("/articles/") == true) {
// Route to your article screen; replace this allowlist first.
}
}
}
}The SDK reports clicks independently of navigation and suppresses duplicate taps within its retention window. It never opens URLs for you. Validate your own scheme, host and path before routing; Notification.isSafeDestination alone is not an app-specific allowlist.
Allow custom schemes under App Settings → General → Mobile deep-link schemes and implement them in your app’s router. See mobile destinations.
7. Test on your own Android device
- Install a build with your real Firebase client configuration and public app key. Grant permission and confirm the Android subscription in the dashboard.
- Send a selected-device test to that installation. For Sending API requests, explicitly include
platforms: ["android"]; omitting platforms still targets Web only. - Check foreground, background and cold-start taps, text and image notifications, receipt/click reports, opt-out/re-opt-in, and offline recovery.
- No registration: check initialization, default Firebase configuration, Google Play services, the package/project pairing, public app key, permission and channel settings.
- Text without an image: the SDK keeps text when image download fails. Images require HTTPS, no redirects, and must fit bounded download and decode limits.
- A tap does not navigate: check launch Activity forwarding and your destination allowlist.
- Rejected registration or storage error: inspect the reported reason. A 409 preserves the existing identity and stops automatic retries. Correct the cause before
Notirabbit.retry(); do not clear app data to bypass an ownership conflict.
Android force-stop and background restrictions can delay or prevent delivery and retries. A reported receipt reflects client rendering/report processing, not guaranteed human viewing.
Send to Android with the APISource and release status
This guide follows SDK revision 6cc54cf. Pin a reviewed revision for repeatable builds. Source access is private; a stable tag and hosted Maven package have not been published.
Automated JVM tests, build/lint checks, independent Maven consumers and emulator storage tests have passed. Physical-device FCM and hardware/OEM acceptance remain open in the SDK’s device acceptance matrix.
The native SDK stores its own encrypted installation state and opt-out separately from Expo. Migrating an existing Expo app requires a deliberate identity and pending-event migration.