Android SDK (Kotlin)

The Android SDK supports Android 7 (API 24) and later. Its only third-party runtime dependency is Google's Play Install Referrer library 2.2 (no AndroidX, no OkHttp, no Play Services in the core).

ArtifactContains
io.x3growth:sdksessions, screens, events, identify, leads, purchase hints, consent with View dialogs, install attribution, deep links
io.x3growth:sdk-replaywireframe recordings for Views and Jetpack Compose
io.x3growth:sdk-composeCompose navigation tracking, masking modifiers, Compose consent sheet and settings

Every method is safe from any thread, never blocks the caller and is processed in call order on one background thread (x3-sdk). Before configure every call is a no-op with a debug log.

Install

app/build.gradle.kts
dependencies {
    implementation("io.x3growth:sdk:1.0.0")
    implementation("io.x3growth:sdk-replay:1.0.0")   // optional: recordings
    implementation("io.x3growth:sdk-compose:1.0.0")  // optional: Compose helpers
}

The artifacts are on Maven Central (mavenCentral() in settings.gradle.kts).

Configure

In your Application class:

app/src/main/java/app/nordlicht/NordlichtApp.kt
package app.nordlicht

import android.app.Application
import io.x3growth.sdk.X3
import io.x3growth.sdk.X3ConsentMode
import io.x3growth.sdk.X3ConsentOptions
import io.x3growth.sdk.X3Options
import io.x3growth.sdk.X3Purposes
import io.x3growth.sdk.X3RecordingMode
import io.x3growth.sdk.X3RecordingOptions

class NordlichtApp : Application() {
    override fun onCreate() {
        super.onCreate()
        X3.configure(this, "pk_3x_YOUR_APP_KEY", X3Options(
            consent = X3ConsentOptions(mode = X3ConsentMode.OPT_IN, purposes = X3Purposes(recordings = true)),
            host = "https://nordlicht.app/n3",
            recording = X3RecordingOptions(mode = X3RecordingMode.WIREFRAME),
        ))
    }
}

Register the class in the manifest (android:name=".NordlichtApp"). consent.mode is required: ALWAYS, OPT_IN or OPT_OUT (consent in apps). All options and defaults: app SDK reference › Options. The SDK keeps its files in noBackupFilesDir/io.x3growth.sdk/ (never backed up, never shared).

Screens

Activities are tracked when they resume (autoScreens, default on); the simple class name becomes the path (PaywallActivity → /paywall). Name a screen yourself:

class PlanActivity : AppCompatActivity(), X3ScreenNameProvider {
    override val x3ScreenName = "Plan"
}

Fragments are not tracked automatically; call X3.screen("Plan") in onResume. With Jetpack Compose Navigation (sdk-compose), report every destination route:

@Composable
fun NordlichtNavHost() {
    val navController = rememberNavController()
    X3TrackNavigation(navController)   // registers once per controller, removed when the composable leaves
    NavHost(navController, startDestination = "home") { /* … */ }
}

Inside a composable always use X3TrackNavigation: it registers the listener in a DisposableEffect. X3Compose.trackNavigation(navController) is the variant for code outside Compose; it returns an X3Subscription that you cancel when the controller goes away (calling it in a composable body would add a listener on every recomposition). Route patterns such as plan/{id} are reported as paths with the placeholder, never the argument values; ids in paths are redacted on the server.

Events

X3.track("plan_selected", mapOf("plan" to "year", "trial" to true, "price" to 59.99))

Same rules as on iOS: lowercase snake_case names, up to 20 properties with string, number or boolean values (reference › track).

Identify

X3.identify(user.id, user.email, null)              // after signup, every login and at start when logged in
X3.track("account_created", mapOf("method" to "google"))
X3.track("login", mapOf("method" to "password"))
X3.reset()                                         // at logout

Users, logins and leads.

Leads

lifecycleScope.launch {
    try {
        X3.lead(X3Lead(form = "consultation", email = email, firstName = name, message = text, marketingConsent = newsletter))
    } catch (error: X3Error) {
        // error.code: X3Error.Code.INVALID, DISABLED, RATE_LIMITED, NETWORK, SERVER, NOT_CONFIGURED
        // subclasses: X3Error.RateLimited (retryAfterSeconds), X3Error.Server (status), …
    }
}

From Java or without coroutines: X3.lead(lead, callback).

Purchases with Play Billing

Pass the account token as obfuscated account id, so Google's notifications name the person, and report the purchase as a hint after Play confirms it:

app/src/main/java/app/nordlicht/Billing.kt
val params = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(listOf(productParams))
    .apply { X3.purchaseAccountToken()?.let { setObfuscatedAccountId(it) } }
    .build()
billingClient.launchBillingFlow(activity, params)

// in onPurchasesUpdated, for Purchase.PurchaseState.PURCHASED:
X3.purchase(X3Purchase(orderId = purchase.orderId ?: return, value = 59.99, currency = "EUR", productId = "pro_yearly"))

The conversion stays held until Google Play (RTDN), RevenueCat or your server confirms the order id GPA.… (purchases and subscriptions).

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    X3.handleIntent(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    X3.handleIntent(intent)
}

handleIntent reads intent.data, marks the intent as handled so it is not counted twice and returns true when the link carried X3 attribution. The SDK also checks the launching intent of each activity once. For X3 links to open the app directly, add an App Links intent filter for your link path and publish assetlinks.json:

app/src/main/AndroidManifest.xml
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="nordlicht.app" android:pathPrefix="/n3/go" />
</intent-filter>
public/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": { "namespace": "android_app", "package_name": "app.nordlicht", "sha256_cert_fingerprints": ["AB:CD:…"] }
}]

Use the SHA-256 fingerprint of your Play app signing key (Play Console › App integrity).

Play Install Referrer

When the purpose attribution is granted, the SDK reads the Play Install Referrer once (within 90 days of the install) on a background thread and sends it with the first open. Any Play link with a referrer= parameter works: X3 links, newsletters, QR codes, partner pages. The server reads X3 click tokens, UTM parameters, gclid and Meta's encrypted install referrer (Play Install Referrer).

Advertising id

The SDK never reads the Google advertising id and removes the permission com.google.android.gms.permission.AD_ID from its manifest (tools:node="remove"). If another SDK in your app needs the advertising id, check the merged manifest (Android Studio › Merged Manifest) and declare your own use in the Play Console. In the Play Console declaration "Does your app use advertising ID?" X3 alone means "No".

Recordings

Add sdk-replay, set recording = X3RecordingOptions(mode = X3RecordingMode.WIREFRAME) and use the purpose recordings. Views and Compose (through the semantics tree) are captured; WebViews, SurfaceView, TextureView, maps and password fields are always masked.

X3.setMasking(cardView, X3Masking.MASK)           // Views
X3.setMasking(priceView, X3Masking.UNMASK)
X3.setAnchor(buyButton, "buy_button")             // tap target name (else the resource entry name)
X3.setAnchorResolver { view -> view.tag as? String }

Text(price, Modifier.x3Unmask())                  // Compose
GoogleMap(modifier = Modifier.x3Mask())
Button(onClick = {}, modifier = Modifier.x3Anchor("buy_button")) { Text("Buy") }   // or Modifier.testTag

Session recordings in apps.

// Views (core artifact): plain dialogs, Material look without a Material dependency, dark mode, TalkBack, 48 dp targets
X3ConsentDialog.show(activity, X3ConsentTexts(appName = "Nordlicht", privacyPolicyUrl = "https://nordlicht.app/privacy")) { state -> }
X3PrivacySettingsDialog.show(activity, X3ConsentTexts())

// Compose (sdk-compose)
X3ConsentSheet(texts = X3ConsentTexts(), onDone = { state -> })
X3PrivacySettings(texts = X3ConsentTexts())

Listeners (X3.addConsentListener { state -> }) run on the main thread. Consent UI helpers.