DEV Community

LinkTrail
LinkTrail

Posted on

Deferred Deep Links in Android: Complete Integration Guide

Same problem as always: a user without your app taps a link, gets routed to the Play Store, installs, opens the app — and lands on the default launch screen with no memory of what they originally tapped. App Links get an existing user routed correctly. They do nothing for the install gap. This is the Android-native (Kotlin) integration guide for closing that gap with LinkTrail, alongside the direct case so both work end to end.

The two cases

  • Direct link — app is already installed. Android hands your Activity an Intent with the link's data, through onCreate (cold start) or onNewIntent (already running).
  • Deferred link — app is not installed. There's no Intent carrying anything, because the app wasn't there to receive it. The match has to happen server-side, keyed off something durable enough to survive the install (device/install-time fingerprint), and get pulled down on first open.

1. Add the dependency

// app/build.gradle
dependencies {
    implementation("com.linktrail:android-sdk:1.4.0")
}
Enter fullscreen mode Exit fullscreen mode

2. Configure App Links in the manifest

This part is identical to correct deep linking in general — deferred linking is built on top of it, not instead of it:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">

    <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="links.yourapp.com" />
    </intent-filter>
</activity>
Enter fullscreen mode Exit fullscreen mode

android:launchMode="singleTask" matters — without it, a second intent while the app is already running can spin up a duplicate Activity instead of hitting onNewIntent on the existing one.

Serve assetlinks.json at https://links.yourapp.com/.well-known/assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourcompany.yourapp",
    "sha256_cert_fingerprints": ["YOUR:APP:SIGNING:CERT:FINGERPRINT"]
  }
}]
Enter fullscreen mode Exit fullscreen mode

Get the SHA-256 fingerprint from your actual release signing config, not your debug keystore — this is the single most common reason App Links verify in testing and silently fail in production:

keytool -list -v -keystore your-release-key.jks -alias your-key-alias
Enter fullscreen mode Exit fullscreen mode

3. Initialize the SDK

In your Application class, as early as possible:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        LinkTrail.configure(this, apiKey = "YOUR_LINKTRAIL_API_KEY")
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Handle the direct case

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        handleIntent(intent)
    }

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

    private fun handleIntent(intent: Intent?) {
        val data: Uri? = intent?.data
        if (data != null) {
            LinkTrail.resolve(data) { link ->
                routeFromLink(link)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

onCreate covers a cold start where the app is launched directly by the link. onNewIntent covers the app already being alive in the background. Both need to be wired — missing either one means links only half-work depending on app state, which is a very confusing bug to chase down later.

5. Handle the deferred case

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    handleIntent(intent)

    // Only relevant on first open after install — a no-op on every
    // subsequent launch, so it's safe to always call.
    LinkTrail.getFirstOpenLink { link ->
        link?.let { routeFromLink(it) }
    }
}
Enter fullscreen mode Exit fullscreen mode

getFirstOpenLink resolves from a server-side match, not from anything the OS handed you — there is no Intent data to read on a cold install, which is exactly the problem it exists to solve.

6. Route on the resolved link

private fun routeFromLink(link: LinkTrailLink) {
    when (link.path) {
        "/product" -> {
            val id = link.params["id"]
            navController.navigate("product/$id")
        }
        "/invite" -> {
            val ref = link.params["ref"]
            navController.navigate("onboarding?referrer=$ref")
        }
        else -> navController.navigate("home")
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Test it properly

  • Direct, cold start: app force-stopped, tap a link, confirm onCreate → handleIntent fires with the right data.
  • Direct, warm: app open in background, tap a link, confirm onNewIntent fires instead — this is the path people forget to test.
  • Deferred: uninstall completely (not just force-stop — App Links verification and any local state need a real uninstall), tap the link, go through the Play Store, install, open. Confirm getFirstOpenLink resolves with the same path.
  • Verify App Links independently of your app, with adb:
adb shell pm get-app-links com.yourcompany.yourapp
Enter fullscreen mode Exit fullscreen mode

If that command doesn't show your domain as verified, nothing above will work, and it's worth checking before you spend time debugging the SDK — the fingerprint or the assetlinks.json host is the far more common culprit.

What's next

This gets you routed correctly, in both states, with the actual link data available. It doesn't cover attribution — matching an install back to which specific campaign or creative drove it — which sits on top of the same underlying resolution and is worth its own post.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.