Skip to documentation

Android SDK

The Kixo Android SDK supports Kotlin 2.0+ and Java, requires minSdk 24 (Android 7.0), and is built against compileSdk 35. Your host app remains responsible for its own targetSdk. A single Kixo.configure call in your Application.onCreate auto-tracks screens, taps, sessions, crashes, and lifecycle events. Push tracking requires the FCM bridge described below. Network request auto-tracking is not part of the current Android release. The SDK also supports session replay, identity, and goals.

Quick start

Three files. Add the Maven repo, add the dependency, then drop two lines into your Application subclass.

Build requirements: compileSdk 35, minSdk 24, Kotlin 2.0+ or Java, and Java 17 bytecode.

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo")
        }
    }
}

Note

That's the whole analytics integration. Standard auto-trackers are enabled by default; push still needs the FCM bridge below. Override individual flags with KixoConfiguration.Builder(...) only when needed.

Add to your app

The Kixo Maven repo is hosted on GitHub Pages. Add it alongside google() and mavenCentral() in settings.gradle.kts:

kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo")
        }
    }
}

Then declare the dependency in your app module:

kotlin
// app/build.gradle.kts
dependencies {
    implementation("io.kixo:kixo-android-sdk:0.1.17")
}

Tip

android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are bundled in the SDK manifest. Notification permissions stay owned by your app and are declared when you opt into push features.

Multi-module projects

Gradle's implementation configuration is not transitive: declaring implementation("io.kixo:kixo-android-sdk:0.1.17") in a library module (e.g. :core_domain) does NOT make Kixo visible to :app or any other consumer. Two patterns work — pick one.

Pattern A — every module that calls Kixo declares it (recommended). Keeps each module's classpath minimal and avoids cascading rebuilds. Use a version catalog (libs.kixo.sdk) so you only edit the version in one place.

kotlin
// :core_domain/build.gradle.kts
dependencies {
    implementation("io.kixo:kixo-android-sdk:0.1.17")   // local use only
}

// :app/build.gradle.kts
dependencies {
    implementation(project(":core_domain"))
    implementation("io.kixo:kixo-android-sdk:0.1.17")   // declared again — fine
}

Pattern B — re-export through api(...). Single declaration, but the library module's public ABI now includes Kixo types — any version bump rebuilds every downstream module. Use this only when the library re-uses Kixo types in its own public signatures (e.g. returns KixoDiagnostics from a function).

kotlin
// :core_domain/build.gradle.kts
dependencies {
    api("io.kixo:kixo-android-sdk:0.1.17")              // re-exposed
}

// :app/build.gradle.kts
dependencies {
    implementation(project(":core_domain"))            // gets Kixo for free
}

Warning

If you see Unresolved reference: Kixo at compile time in a module, that module is missing its own dependency on the SDK — add the implementation line above, or use Pattern B.

Initialize

Configure Kixo from your Application subclass — onCreate runs before any activity, so every screen view, tap, and lifecycle event is captured from the first frame. Register the Application in your manifest with android:name=".MyApp".

kotlin
import android.app.Application
import io.kixo.sdk.Kixo

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Kixo.configure(
            context   = this,
            projectId = "kx_proj_YOUR_PROJECT_ID",
            apiKey    = "kx_key_YOUR_API_KEY",
        )
    }
}

For fine-grained tunables (auto-track flags, flush cadence, replay sampling, custom API host) build a KixoConfiguration explicitly:

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.KixoConfiguration

val config = KixoConfiguration.Builder(
    projectId = "kx_proj_YOUR_PROJECT_ID",
    apiKey    = "kx_key_YOUR_API_KEY",
)
    .autoTrackScreens(true)
    .autoTrackTaps(true)
    .autoTrackNetwork(false)  // reserved; no-op in the current Android release
    .autoTrackCrashes(true)
    .autoTrackSessions(true)
    .autoTrackPush(true)
    .flushIntervalMillis(30_000)
    .flushAt(20)
    .maxBufferSize(200)
    .build(applicationContext)

Kixo.configure(this, config)

Note

Idempotent. A second configure call from the same process is a WARN-logged no-op — the SDK keeps the first configuration. Events queued by your auth singleton before configure lands are buffered (cap 50) and replayed once the SDK is wired up, so you can call Kixo.identify(...) from a global before Application.onCreate finishes.

Track events

Three primitives carry the bulk of your instrumentation: track for events, markGoal for conversion signals, and addBreadcrumb for non-event context.

kotlin
import io.kixo.sdk.Kixo

Kixo.track("video_played", mapOf(
    "video_id"    to "vid_42",
    "duration_ms" to 18_500,
    "autoplay"    to false,
))

// markGoal(name, value?, currency?, properties?) — pass extra context
// through the named 'properties' argument (a Map can't be the 2nd
// positional arg; that slot is the Double 'value').
Kixo.markGoal("activated", properties = mapOf(
    "step" to "onboarding_completed",
))

// Revenue goals use the typed value + currency parameters:
Kixo.markGoal("purchase_completed", value = 49.99, currency = "USD")

Kixo.addBreadcrumb(
    message  = "user toggled dark mode",
    category = "ui",
    level    = "info",
)

Tip

Goals are graded. Marked goals feed Kixo's activation funnels and the daily change-detection cron — a goal whose volume drops 70% week-over-week shows up in your dashboard with a Needs review badge. Use markGoal for the handful of moments that matter; track for everything else.

Standard events

Sugar over Kixo.track for the events Kixo recognizes by name — verbatim string keys that the backend's standard-event detector matches. Compile-time validation of property shape, single source of truth on naming.

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.SubscriptionInterval

Kixo.trackPurchase(
    amount    = 49.99,
    currency  = "USD",
    productId = "pro_yearly",
)

Kixo.trackSubscriptionStart(
    plan     = "pro",
    amount   = 9.99,
    currency = "USD",
    interval = SubscriptionInterval.MONTH,
)

Kixo.trackSignup(method = "google")
Kixo.trackTrialStart(plan = "pro", days = 14)
Kixo.trackCancel(plan = "pro", reason = "too_expensive")
Kixo.trackUpgrade(fromPlan = "free", toPlan = "pro")
Kixo.trackActivation(event = "first_post_published")
Kixo.trackShare(channel = "twitter", contentId = "post_123")
Kixo.trackInvite(channel = "email", recipientCount = 5)

Identify users

Bind subsequent events to a stable user id and a bag of traits. Anonymous-to-known stitching happens in Kixo — events captured before identify are retroactively attributed to the same user.

kotlin
import io.kixo.sdk.Kixo

// Reserved standard property keys carry a $-prefix (Mixpanel
// convention) so they namespace away from your own custom traits
// and promote to the dashboard's profile columns. See
// io.kixo.sdk.StandardProperty for the typed catalogue, or the
// "Standard property catalog" section below for the full 37-key list.
Kixo.identify("user_123", mapOf(
    "$email" to "jane@example.com",     // identity
    "$name"  to "Jane Doe",              // identity
    "$plan"  to "pro",                   // subscription pack
    "$lifetime_orders" to 12,            // e-commerce pack
    "signup_source" to "twitter_ad",     // custom trait
))

// Logout: clear identity, super-properties, and the persisted queue.
Kixo.reset()

⚠️ Kotlin dollar-sign trap. Standard identity keys are $-prefixed ($email, $name, $first_name) — and in a Kotlin literal you must escape the dollar as "\$email". Writing "$email" string-interpolates your email variable, so the value silently lands as a custom trait and never populates the Audience email / name columns. Simplest fix — use the typed overload (SDK 0.1.13+), which is impossible to get wrong: Kixo.setUserProperty(StandardProperty.EMAIL, email).

Tag a user for segmentation

Use setUserProperty with a boolean value to attach a simple yes/no tag to the user. The tag persists across launches and powers segments, email campaigns, and chat queries — no setup beyond the SDK call.

kotlin
// Tag a user as subscribed — segments + campaigns can target this
Kixo.setUserProperty("subscribe", true)

// VIP membership
Kixo.setUserProperty("vip", true)

// String + numeric values work too
Kixo.setUserProperty("plan_tier", "enterprise")
Kixo.setUserProperty("lifetime_orders", 42)

// Bulk-set
Kixo.setUserProperties(mapOf(
    "subscribe" to true,
    "plan_tier" to "enterprise",
))

Properties persist via SharedPreferences across launches and auto-attach to every outbound event. In chat say things like "send a welcome email to users where subscribe is true" — Kixo builds the segment and drafts the template for you. Cleared on Kixo.reset().

Standard property catalog

Reserved property keys carry a $ prefix so they namespace away from your custom traits. Kixo's catalog covers 37 keys across 3 universal packs (identity, geo, lifecycle) and 5 B2B vertical packs (subscription, e-commerce, media, marketplace, loyalty). Set whichever apply to your product — the dashboard adapts and renders only the packs you populate.

Identity

Always relevant. Sets the profile header columns.

KeyTypeDescription
$emailstringPrimary email, often the merge key for identity stitching.
$phonestringE.164 phone number.
$namestringFull display name.
$first_namestringGiven name.
$last_namestringFamily name.
$avatar_urlstringFull URL to the user's avatar image.

Geo

Geographic context.

KeyTypeDescription
$countrystringISO 3166 country code.
$citystringCity name.
$regionstringState or province.
$timezonestringIANA zone like America/Los_Angeles.
$languagestringIETF tag like en or ru-RU.
$localestringFull locale identifier.

Lifecycle

When did we see them.

KeyTypeDescription
$createdISO8601Signup or account creation time.
$last_seenISO8601Last engagement time.

Subscription

Set if your product has plans.

KeyTypeDescription
$planstringTier slug — free, pro, enterprise.
$subscription_statusstringactive / trial / cancelled / past_due.
$trial_endsISO8601When the current trial expires.
$mrrnumberMonthly recurring revenue in account currency.
$subscription_startedISO8601When the current subscription began.

E-commerce

Set if you sell products.

KeyTypeDescription
$lifetime_ordersnumberCount of completed orders.
$lifetime_revenuenumberTotal spend.
$aovnumberAverage order value.
$last_purchaseISO8601Most recent successful purchase.
$first_purchaseISO8601First successful purchase.
$cart_abandoned_countnumberLifetime count of cart abandonments.

Media

Set if you publish content.

KeyTypeDescription
$content_tierstringfree / premium / paid.
$subscribed_categoriesCSV string or arrayCategories the user follows.
$watch_time_totalnumberLifetime watch time in seconds.
$last_playedISO8601Most recent playback start.

Marketplace

Set if you're a two-sided platform.

KeyTypeDescription
$seller_tierstringSeller-side tier slug.
$buyer_tierstringBuyer-side tier slug.
$listings_countnumberActive listings the user owns.
$reviews_countnumberReviews the user has received.
$verifiedbooleanKYC status.

Loyalty

Set for engagement and rewards programs.

KeyTypeDescription
$loyalty_pointsnumberCurrent redeemable points balance.
$vip_levelstringVIP tier slug.
$referral_countnumberSuccessful referrals attributed to this user.

Tip

Don't see your pattern? Use bare keys for custom traits. They surface in the dashboard's Custom Traits panel without polluting the profile columns. The 5 vertical packs above are opinionated guesses at the most common B2B shapes — customer-specific terminology (e.g. shipping_plan) stays bare.

Super-properties

Per-session key/value pairs auto-attached to every outbound event. Different from identify traits (which describe identity); super-properties describe session context — active A/B variant, build flavor, opted-in feature flags. Persisted across launches; cleared on reset(). Per-event properties on track always win on collision.

kotlin
import io.kixo.sdk.Kixo

Kixo.setSuperProperty("build_flavor", "beta")
Kixo.setSuperProperties(mapOf(
    "ab_variant"        to "B",
    "referrer_campaign" to "autumn-launch",
))

// Sugar for A/B tracking — stored as 'experiment_<id>'.
Kixo.setExperimentVariant("checkout_v2", "variant_a")

Kixo.unsetSuperProperty("build_flavor")
Kixo.clearSuperProperties()

Push notifications

Two integration paths. Pick A if you're on FCM and want the shortest working setup; pick B if you already have a custom FirebaseMessagingService you can't restructure or you want explicit control over which FCM deliveries Kixo sees.

Option A — extend KixoFirebaseMessagingService (auto-tracking)

Subclass KixoFirebaseMessagingService and call super.onMessageReceived(...) from your override — Kixo auto-emits push_received (visible payload) or push_silent (data-only). The base class also handles onNewToken registration if you don't override it. AndroidManifest.xml registration is unchanged from a normal FCM service.

kotlin
import com.google.firebase.messaging.RemoteMessage
import io.kixo.sdk.KixoFirebaseMessagingService

class MyMessagingService : KixoFirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)  // Kixo auto-tracks push_received
        // … your own routing / notification display
    }
}

Note

Kixo compiles this optional class against Firebase Messaging but does not add Firebase to your app transitively. The SDK declares Firebase as compileOnly; an app choosing this option must already depend on firebase-messaging, as any FCM receiver does.

Option B — call manual API from your own FCM service

Register your FCM token with Kixo via FirebaseMessagingService.onNewToken, then log each delivery explicitly. Use this path when you want Kixo to see only a subset of FCM deliveries. Android delivery currently supports FCM only.

kotlin
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.kixo.sdk.Kixo
import io.kixo.sdk.PushProvider

class MyMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        Kixo.setPushToken(token, PushProvider.FCM)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        // Convert the FCM payload to a Map<String, Any?> and log it —
        // Kixo correlates this with the open / dismiss it sees later.
        Kixo.logPushReceived(message.data.toMap(), appState = "background")
    }
}

Android does not expose a universal lifecycle hook for notification opens, dismissals, or action buttons. Forward those signals from the notification intents or receivers your app creates:

kotlin
import io.kixo.sdk.Kixo

Kixo.logPushOpened(payload = pushPayload)            // open
Kixo.logPushOpened(payload = pushPayload, actionId = "reply")  // action-button tap
Kixo.logPushDismissed(payload = pushPayload)         // swipe-away

Session replay

Replay a real, visual reconstruction of what the user saw. On each capture the SDK encodes a compressed frame of the screen (a JPEG image) together with a structural snapshot of the view hierarchy, and uploads both — so the dashboard player can render pixel-accurate playback alongside the interaction timeline. Configure replay for the project in Dashboard → Settings → Session replay. The SDK automatically reads and refreshes that project policy, including masking, capture modes, and cellular-upload permission.

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.KixoConfiguration

val config = KixoConfiguration.Builder(
    projectId = "kx_proj_YOUR_PROJECT_ID",
    apiKey    = "kx_key_YOUR_API_KEY",
)
    .build(applicationContext)

Kixo.configure(this, config)

When cellular upload is disabled, queued replay waits for an allowed network.

Tip

Mask before upload. Kixo captures pixels, so masking runs before anything leaves the device. Password and e-mail fields are auto-detected and redacted; text in structural snapshots passes through a PII filter; and any view you mark with setKixoMask(true) is rasterized to an opaque rectangle in the frame before the JPEG is encoded — its pixels never leave the device. Jetpack Compose screens are masked wholesale by default (call setKixoMask(false) on the outermost ComposeView to include a screen you have audited). Operators scrub the replay player alongside the event timeline in the dashboard.

Data collection

The SDK captures the data enabled in your project and the events and properties your application sends.

Debugging

Kixo.diagnostics() returns a read-only snapshot of the SDK's health — useful in a hidden debug screen or smoke test. Answers "why aren't my events flowing?" without a debugger.

kotlin
import io.kixo.sdk.Kixo

val diag = Kixo.diagnostics()
Log.d("Kixo", "queued=${diag.queue.bufferedEventCount}")
Log.d("Kixo", "paused=${diag.paused}")                  // collection paused state
Log.d("Kixo", "lifecycleState=${diag.lifecycleState}")  // SDK lifecycle state

Force a flush from your test harness — blocks up to timeoutMs on a network round-trip:

kotlin
import io.kixo.sdk.Kixo

// Async fire-and-forget — returns immediately.
Kixo.flush()

// Blocking variant for instrumentation tests. Never call on the main thread.
val landed: Boolean = Kixo.flushBlocking(timeoutMs = 5_000L)
assertTrue(landed)

Compose Navigation

Activity / Fragment routes produce immediate screen_viewevents and structured screen_visit records with dwell and flow metadata out of the box. For Jetpack Compose Navigation, fire Kixo.screen from a LaunchedEffect keyed on the route — the SDK then sees one event per destination, regardless of recomposition count.

kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import io.kixo.sdk.Kixo

@Composable
fun AppNavHost(nav: NavHostController) {
    NavHost(navController = nav, startDestination = "home") {
        composable("home") {
            LaunchedEffect("home") { Kixo.screen("HomeScreen") }
            HomeScreen()
        }
        composable("settings") {
            LaunchedEffect("settings") { Kixo.screen("SettingsScreen") }
            SettingsScreen()
        }
    }
}

AI coding agents

The SDK's public surface is small and shaped for code-completion — every method is on the Kixo singleton, every Kotlin sample in this guide starts with import io.kixo.sdk.Kixo, and our README ships an "AI agent quick reference" block that tools like Claude Code, Cursor, and Codex can paste directly into their context. If your agent gets stuck, the canonical starter is:

kotlin
// Tell your AI coding agent:
// "Integrate the Kixo Android SDK using io.kixo:kixo-android-sdk
//  from https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo.
//  Call Kixo.configure(this, projectId, apiKey) in Application.onCreate.
//  Then use Kixo.track / Kixo.identify / Kixo.markGoal as needed."

Note

Each section above has been written with that workflow in mind — imports are always explicit, types are always named, and the SDK singleton is never aliased. Hand this page to your agent and let it drive.