Implementation of Deep Linking in Mobile Application
Deep Link is a URL that opens specific screen in mobile app. Not homepage, but exactly needed content: product, profile, article, payment page. Seems simple until you face difference between Custom URL Scheme, Universal Links, App Links, Deferred Deep Links and how each breaks in its specific place.
Three types of deep links
Custom URL Scheme (myapp://product/123) — simplest but most unreliable. If app not installed — browser shows error, no fallback. Multiple apps can register one scheme — unpredictable which opens. Suitable only for internal scenarios: cross-app communication within own ecosystem.
Universal Links (iOS) / App Links (Android) — HTTP/HTTPS links that operating system intercepts and opens in app instead of browser. If app not installed — normal browser. Reliable, verified, correct fallback. This is production standard.
Deferred Deep Links — link saved even if app not installed. User taps link → App Store → installs app → opens → lands on right screen. Implemented through Firebase Dynamic Links (deprecated since 2025-08), Branch.io or Adjust.
Universal Links on iOS
Requires apple-app-site-association file on server:
// https://yourdomain.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["TEAMID.com.company.app"],
"components": [
{ "/": "/product/*", "comment": "Product pages" },
{ "/": "/profile/*" },
{ "/": "/order/*" },
{ "/": "/promo/*", "?": { "ref": "?" } }
]
}
]
}
}
AASA must be returned with Content-Type: application/json, without redirects, with code 200. Apple CDN caches it aggressively — changes take effect with delay up to 48 hours (Apple periodically visits AASA in background). On iOS 16+ added "mode": "developer" for faster AASA update in debug.
In Info.plist — Associated Domains:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourdomain.com</string>
<string>applinks:www.yourdomain.com</string>
</array>
Handling in SceneDelegate:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
DeepLinkRouter.shared.handle(url: url)
}
App Links on Android
Analog of Universal Links. File assetlinks.json on server:
// https://yourdomain.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.company.app",
"sha256_cert_fingerprints": ["AA:BB:CC:..."]
}
}]
SHA256 fingerprint taken from keystore: keytool -list -v -keystore release.jks. Debug and release — different fingerprints, both needed in production assetlinks.json or use different domains.
In 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="yourdomain.com"
android:pathPrefix="/product/" />
</intent-filter>
</activity>
android:autoVerify="true" launches domain verification on install. Check verification: adb shell pm get-app-links com.company.app. If STATE_APPROVED — App Links work. If STATE_NO_RESPONSE or STATE_FAILED_VERIFICATION — problem with AASA or certificate.
Common problem on Android 12+: even with successful verification user can choose "Open in browser" in system settings. App cannot control this programmatically.
Router on client
Centralized router — mandatory. No if (url.contains("product")) scattered through code.
// Android
class DeepLinkRouter {
fun handle(intent: Intent, navController: NavController) {
val uri = intent.data ?: return
val path = uri.path ?: return
val route = when {
path.matches(Regex("/product/(\\d+)")) -> {
val productId = uri.lastPathSegment ?: return
Route.ProductDetail(productId)
}
path.matches(Regex("/order/(\\w+)")) -> {
Route.OrderDetail(uri.lastPathSegment ?: return)
}
path == "/profile" -> Route.Profile
path.startsWith("/promo/") -> {
val ref = uri.getQueryParameter("ref")
Route.Promo(uri.lastPathSegment ?: return, ref)
}
else -> {
// Unknown route — open in browser
openInBrowser(uri)
return
}
}
navController.navigate(route)
}
}
// iOS
final class DeepLinkRouter {
func handle(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
let pathComponents = components.path.split(separator: "/").map(String.init)
switch pathComponents.first {
case "product" where pathComponents.count == 2:
navigationManager.push(.productDetail(id: pathComponents[1]))
case "order" where pathComponents.count == 2:
navigationManager.push(.orderDetail(id: pathComponents[1]))
case "profile":
navigationManager.push(.profile)
default:
UIApplication.shared.open(url) // fallback to browser
}
}
}
Deferred Deep Links through Branch.io
Firebase Dynamic Links officially deprecated since August 2025. Alternatives: Branch.io, Adjust, AppsFlyer, Airbridge — all provide SDK for iOS and Android.
Branch.io SDK tracks click before install and restores parameters after first launch:
Branch.getInstance().initSession(
branchReferralInitListener = { params, error ->
if (error == null && params != null) {
val productId = params.getString("product_id")
val ref = params.getString("ref")
if (productId != null) {
deepLinkRouter.navigate(Route.ProductDetail(productId))
}
}
},
isReferrable = true,
activity = this
)
Branch initSession called on each launch — SDK determines whether this is organic launch or link transition.
Testing
App Links verification: adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.company.app
Universal Links verification on simulator — impossible. Only on physical device via xcrun simctl openurl booted "https://yourdomain.com/product/123" for iOS 14+ simulator, or via Notes.app → tap link.
Check AASA: curl -I https://yourdomain.com/.well-known/apple-app-site-association — needs 200, without redirects.
Apple provides validator: https://app-site-association.cdn-apple.com/a/v1/yourdomain.com — how Apple caches your AASA file.
Typical errors
Redirect on CDN breaks App Links. If yourdomain.com redirects to www.yourdomain.com, and AASA on one only — verification fails. AASA needed on both domains.
Wrong Content-Type. AASA with Content-Type: text/html — Apple doesn't accept. Only application/json.
Deep link on cold start vs warm start. On Android Intent comes in onCreate on cold start and in onNewIntent on warm start. If handling only in onCreate — deep link on warm start ignored. Handle in both places.
Navigation before initialization. Router tries navigate before NavController ready. Need queue of pending deep links, processed after navigation initialization.
Implementation of Universal Links + App Links + router + deferred deep links: 2-4 weeks. Cost calculated individually.







