Kotlin SDK for Android — event logging, structured metrics, funnels, identity,
screen tracking, a drop-in user feedback view, and in-app questionnaires, all
delivered to your own Pubky Pulse server. It mirrors the
Swift SDK feature-for-feature with
Android-native idioms. The core module has a single runtime dependency
(kotlinx-coroutines, the analog of Swift Concurrency); the optional Jetpack
Compose UI lives in a separate artifact so non-Compose apps stay lean.
Full setup guide & API reference: pubkypulse.com/docs/sdks/android
Add the core module always; add the Compose module only if you want the drop-in UI.
| Artifact | Purpose |
|---|---|
org.pubky.pulse:pulse-android |
Core SDK — analytics, metrics, funnels, identity, plus the programmatic feedback and questionnaire APIs. Framework-only + coroutines. |
org.pubky.pulse:pulse-android-compose |
Optional Jetpack Compose UI — PulseFeedbackView, PulseQuestionnaireGate / PulseQuestionnaireView, and the Modifier.pulseScreen screen-tracking modifier. |
dependencies {
implementation("org.pubky.pulse:pulse-android:0.1.0")
// optional drop-in Compose UI:
implementation("org.pubky.pulse:pulse-android-compose:0.1.0")
}See releases for the latest version.
Configure the SDK once, as early as possible — Application.onCreate() is the
right place.
import android.app.Application
import org.pubky.pulse.android.Pulse
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Pulse.configure(
context = this,
apiKey = "pulse_client_...",
)
Pulse.info("app_launched")
}
}Register the Application subclass in your manifest:
<application android:name=".MyApp" ... >configure validates its input and throws PulseConfigurationError on an
invalid endpoint, API key, or missing package name.
endpoint is optional and defaults to https://ingest.pubkypulse.com, Pubky's
own hosted ingest host. The fallback is silent — nothing is logged or thrown —
so if you run your own server, pass endpoint = "https://ingest.example.com"
explicitly or your events go to Pubky's instance instead of yours.
Pulse.info("feed_loaded", screenName = "Feed")
Pulse.warn("cache_miss", attributes = mapOf("key" to "user_profile"))
// Attribute values may be null — null entries are dropped before the event
// ships, so optional strings flow through without unwrapping at the call site.
Pulse.info("draft_created", attributes = mapOf("draftId" to session.draftId))
// Report a Throwable — the runtime type, stack trace, and cause chain are
// extracted into `_error_*` attributes the server uses to cluster issues.
try {
upload()
} catch (e: Exception) {
Pulse.error(e, message = "while uploading photos")
}The SDK stamps events with an anonymous device id until you identify the user.
Call setUser after login; previously-sent anonymous events are retroactively
claimed for the real user.
Pulse.setUser("user_12345")
Pulse.setUserProperties(mapOf("plan" to "premium"))
// On logout — reverts to the anonymous id for future events.
Pulse.clearUser()
// On a shared device, mint a fresh anonymous id:
Pulse.clearUser(newAnonymousId = true)val op = Pulse.startOperation("photo-upload", attributes = mapOf("format" to "heic"))
// … do work …
op.complete(attributes = mapOf("size_kb" to "512"))
// or op.fail(error = "network")
// or op.cancel()
// Single-shot metric (no lifecycle):
Pulse.recordMetric("checkout")Pulse.step("welcome-screen")
Pulse.step("create-account")
Pulse.step("first-post")import org.pubky.pulse.android.compose.pulseScreen
@Composable
fun HomeScreen() {
Column(modifier = Modifier.pulseScreen("Home")) { ... }
}pulseScreen emits a screen-appeared event on enter and a screen-disappeared
event with the visible duration on exit.
Drop PulseFeedbackView into a ModalBottomSheet, a full screen, or an embedded
section — the host owns presentation and dismissal:
import androidx.compose.material3.ModalBottomSheet
import org.pubky.pulse.android.compose.PulseFeedbackView
if (showFeedback) {
ModalBottomSheet(onDismissRequest = { showFeedback = false }) {
PulseFeedbackView(
onSubmitted = { showFeedback = false },
onCancel = { showFeedback = false },
)
}
}Every label, placeholder, and error message is overridable via
PulseFeedbackStrings. To submit from your own form instead, call
Pulse.sendFeedback(message = …, email = …).
Wrap a screen in PulseQuestionnaireGate to auto-present a questionnaire when
its trigger fires and the server reports the user eligible. The SDK saves partial
drafts as the user advances and resumes a half-finished questionnaire mid-flow:
import org.pubky.pulse.android.PulseQuestionnaireTrigger
import org.pubky.pulse.android.compose.PulseQuestionnaireGate
PulseQuestionnaireGate(
slug = "post-onboarding-nps",
trigger = PulseQuestionnaireTrigger.afterLaunch,
) {
HomeScreen()
}For full control, fetch with Pulse.fetchQuestionnaire(slug) and submit with
Pulse.saveQuestionnaireResponse(slug, answers, isComplete), rendering
PulseQuestionnaireView or your own UI.
The SDK collects analytics events, diagnostics/crash data, and product
interaction, plus — only when you opt in — a user id (Pulse.setUser) and
feedback name/email (the PulseFeedbackView contact fields). It is not
used for tracking or advertising (no Advertising ID, no AD_ID permission), is
not shared with third parties (events go only to the configured ingest
endpoint — your own server, or Pubky's hosted host when you leave endpoint
unset), and is encrypted in transit over HTTPS.
Before publishing, complete Google Play's Data safety form. The SDK ships a guide that maps each SDK behavior to the exact form entries:
The SDK stores a stable anonymous id (pulse_anon_*) in a private
SharedPreferences file named org.pubky.pulse.sdk. On iOS the equivalent id
lives in the Keychain and survives an app delete + reinstall; Android wipes a
package's SharedPreferences on uninstall, and there is no framework-only
way for the SDK to replicate that. By default a returning user is therefore
minted a fresh anonymous id after a reinstall.
If you want best-effort reinstall persistence, opt that prefs file into Android's
Auto Backup. The SDK ships two ready-made rule files — reference them from your
app's <application> tag:
<application
android:fullBackupContent="@xml/pubky_pulse_backup_rules"
android:dataExtractionRules="@xml/pubky_pulse_data_extraction_rules"
... >@xml/pubky_pulse_backup_rules— legacyfullBackupContent(Android 6–11).@xml/pubky_pulse_data_extraction_rules— Android 12+ cloud-backup + device-transfer.
Both include only the org.pubky.pulse.sdk prefs file, so the anonymous id can
roam to a reinstall via the user's own Google backup. This is best-effort
(Auto Backup is opportunistic and user-disablable) and opt-in — the SDK
cannot force the host app's backup config. If your app already defines its own
backup rules, merge the SDK's
<include domain="sharedpref" path="org.pubky.pulse.sdk.xml" /> lines into them
rather than overwriting.
- minSdk 24 (Android 7.0)
- Kotlin 2.0+, AGP 8.7+, JDK 17+
- The core module merges two install-time, no-prompt permissions into your app:
INTERNETandACCESS_NETWORK_STATE.
./gradlew assemble # build all modules
./gradlew test # JVM unit tests (Robolectric)MIT. See LICENSE.