Skip to content

Android SDK

Add the Trace SDK to your module-level build.gradle.kts:

All Android apps need the core SDK:

dependencies {
implementation("io.traceclick:trace-sdk:<version>")
}

Initialize Trace in your Application class. This must happen before any activity launches.

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Trace.initialize(
context = this,
config = TraceConfig(
apiKey = "tr_live_xxxxxxxxxxxx",
hashSalt = "your_64_char_hex_salt",
region = Region.US // or Region.EU
)
)
}
}

The SDK automatically performs attribution on first launch — no additional call needed.

Choose the approach that matches your app’s architecture.

For apps that don’t use Jetpack Compose, set a listener and forward intents manually.

  1. Set a deep link listener in your Application.onCreate() (or wherever you initialize):

    Trace.setDeepLinkListener { deepLink ->
    // deepLink.path — e.g. "/product/123"
    // deepLink.params — e.g. {"color": "blue"}
    // deepLink.isDeferred — true if delivered via install attribution
    navigateTo(deepLink.path, deepLink.params)
    }
  2. Forward intents in your launcher Activity so the SDK can process direct deep links:

    class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    TraceAndroid.handleIntent(intent)
    // ... set up your UI
    }
    override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    TraceAndroid.handleIntent(intent)
    }
    }

For Compose apps that use their own navigation (or no navigation library), wrap your root composable with TraceProvider. It handles intent forwarding automatically — no need to call handleIntent in your Activity.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TraceProvider {
App()
}
}
}
// No handleIntent needed — TraceProvider handles it.
}

Use rememberDeepLinkMapper to define how deep link paths map to your app’s routes or actions:

@Composable
fun App() {
val mapper = rememberDeepLinkMapper {
route("/product/{id}") { params ->
ProductRoute(id = params.require("id"))
}
route("/settings") { SettingsRoute }
action("/claim/promo/{amount}") { params ->
claimReward(params.require("amount"))
}
}
// Use the mapper to handle incoming deep links however your app navigates.
val trace = LocalTrace.current
val deepLink by trace.deepLink.collectAsStateWithLifecycle()
LaunchedEffect(deepLink) {
val link = deepLink ?: return@LaunchedEffect
when (val result = mapper(link)) {
is DeepLinkResult.Navigate -> navigateTo(result.route)
is DeepLinkResult.Action -> result.execute()
null -> { /* unrecognized path */ }
}
trace.consumeDeepLink()
}
}

If your app requires login before navigating to a deep link destination, use TracePostAuthEffect to drain parked deferred deep links after authentication:

@Composable
fun App() {
val isLoggedIn by authViewModel.isLoggedIn.collectAsStateWithLifecycle()
TraceProvider {
// Deep links that arrive before auth is ready are parked automatically.
// This effect drains them once the user is logged in.
TracePostAuthEffect(
isAuthenticated = isLoggedIn,
onDeepLink = { deepLink ->
navigateTo(deepLink.path)
}
)
AppContent()
}
}

For apps using Jetpack Navigation3, the io.traceclick:trace-sdk-nav3 artifact provides a decorator that integrates directly with your NavDisplay back stack.

import com.trace.sdk.compose.TraceProvider
import com.trace.sdk.compose.rememberDeepLinkMapper
import com.trace.sdk.nav3.rememberTraceEntryDecorator
import com.trace.sdk.nav3.navigateReplacing
  1. Define your route mapper with rememberDeepLinkMapper:

    val mapper = rememberDeepLinkMapper {
    route("/product/{id}") { params ->
    ProductRoute(id = params.require("id"))
    }
    route("/invite/{code}") { params ->
    InviteRoute(code = params.require("code"))
    }
    route("/checkout") { params ->
    CheckoutRoute(promo = params["promo"])
    }
    action("/claim/promo/{amount}") { params ->
    claimReward(params.require("amount"))
    }
    }
  2. Create the entry decorator and wire it into NavDisplay:

    @Composable
    fun AppNavigation(authViewModel: AuthViewModel) {
    val backStack = rememberNavBackStack(HomeRoute)
    val isLoggedIn by authViewModel.isLoggedIn.collectAsStateWithLifecycle()
    val mapper = rememberDeepLinkMapper {
    route("/product/{id}") { ProductRoute(id = it.require("id")) }
    route("/invite/{code}") { InviteRoute(code = it.require("code")) }
    route("/checkout") { CheckoutRoute(promo = it["promo"]) }
    action("/claim/promo/{amount}") { /* claimReward(it.require("amount")) */ }
    }
    TraceProvider {
    NavDisplay(
    backStack = backStack,
    onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
    entryDecorators = listOf(
    rememberSaveableStateHolderNavEntryDecorator(),
    rememberTraceEntryDecorator(
    backStack = backStack,
    routeMapper = mapper,
    authGate = { isLoggedIn }
    ),
    ),
    entryProvider = entryProvider {
    entry<HomeRoute> { HomeScreen(/* ... */) }
    entry<ProductRoute> { route -> ProductScreen(id = route.id) }
    // ... other entries
    }
    )
    }
    }

The Nav3 integration provides two back stack extensions in com.trace.sdk.nav3:

  • navigateReplacing(destination) — pushes destination, replacing everything above the root. Used for deferred deep links so the user doesn’t back-navigate to a blank launch screen.
  • navigateSingleTop(destination) — pushes destination only if it isn’t already the top of the stack. Used for direct deep links when the app is already open.

The decorator uses these automatically, but you can also use them directly:

backStack.navigateReplacing(HomeRoute)

The params object in your route mapper provides typed accessors:

route("/product/{id}") { params ->
val id = params.require("id") // throws if missing
val color = params["color"] // nullable String
val page = params.int("page") // Int?
val premium = params.boolean("premium") // Boolean?
ProductRoute(id, color, page, premium)
}

To react to the attribution result directly (e.g. for analytics):

Trace.setAttributionListener { result ->
when (result) {
is AttributionResult.Attributed -> {
analytics.track("install_attributed", mapOf(
"method" to result.method,
"campaign" to result.campaignId
))
}
is AttributionResult.Organic -> {
analytics.track("install_organic")
}
is AttributionResult.Error -> {
log.warn("Attribution failed: ${result.message}")
}
}
}

Track post-install events to measure campaign effectiveness:

Trace.trackEvent(
name = "purchase_completed",
properties = mapOf(
"value" to "49.99",
"currency" to "USD",
"product_id" to "SKU_123"
)
)

See Event Tracking for best practices.

Respect user consent by toggling data collection at runtime. The preference is persisted — you only need to call it once.

// After user opts out (e.g. in a settings screen)
Trace.setEnabled(false)
// Re-enable later
Trace.setEnabled(true)
// Check current state
if (Trace.isEnabled) { /* ... */ }

You can also disable collection at init time if you check consent before onCreate:

Trace.initialize(
context = this,
config = TraceConfig(
apiKey = "tr_live_xxxxxxxxxxxx",
hashSalt = "...",
enabled = hasUserConsent() // false = no data collected
)
)

See Configuration — Privacy opt-out for full details.

To support direct deep links (not just deferred), two things are needed: an intent filter in your manifest, and a Digital Asset Links file on your Trace subdomain.

Tell Trace your app’s signing certificate so it can serve the assetlinks.json file automatically:

Terminal window
# Get your SHA-256 fingerprint
keytool -list -v -keystore your-keystore.jks | grep SHA256
# Register it with Trace
trace apps update --android-cert-fingerprint "AA:BB:CC:..."

Add your Trace domain to AndroidManifest.xml:

<activity android:name=".MainActivity">
<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="yourapp.traceclick.io"
android:pathPrefix="/l/" />
</intent-filter>
</activity>

Android will verify the domain by fetching https://yourapp.traceclick.io/.well-known/assetlinks.json — Trace handles this automatically.