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
Intentwith the link's data, throughonCreate(cold start) oronNewIntent(already running). -
Deferred link — app is not installed. There's no
Intentcarrying 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")
}
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>
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"]
}
}]
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
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")
}
}
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)
}
}
}
}
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) }
}
}
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")
}
}
7. Test it properly
-
Direct, cold start: app force-stopped, tap a link, confirm
onCreate→handleIntentfires with the rightdata. -
Direct, warm: app open in background, tap a link, confirm
onNewIntentfires 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
getFirstOpenLinkresolves with the same path. -
Verify App Links independently of your app, with
adb:
adb shell pm get-app-links com.yourcompany.yourapp
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.