Реалізація Deep Linking у мобільному додатку
Deep Link — це URL, який відкриває конкретний екран у мобільному додатку. Не головну сторінку, а саме потрібний контент: товар, профіль, статтю, сторінку оплати. Здавалось би просто — поки не стикнешся з різницею між Custom URL Scheme, Universal Links, App Links, Deferred Deep Links та тим, як кожен із них ломається в своєму специфічному місці.
Три типи deep links
Custom URL Scheme (myapp://product/123) — найпростіший, але найненадійніший. Якщо додаток не встановлено — браузер показує помилку, ніякого fallback. Кілька додатків можуть зареєструвати одну схему — непередбачувано, яка відкриється. Підходить тільки для внутрішніх сценаріїв: крос-апп комунікація в межах своєї екосистеми.
Universal Links (iOS) / App Links (Android) — HTTP/HTTPS посилання, які операційна система перехоплює та відкриває в додатку замість браузера. Якщо додаток не встановлено — звичайний браузер. Надійно, верифіковано, правильний fallback. Це стандарт для production.
Deferred Deep Links — посилання зберігається навіть якщо додаток не встановлено. Користувач натискає посилання → App Store → встановлює додаток → відкриває → потрапляє на потрібний екран. Реалізується через Firebase Dynamic Links (deprecated з 2025-08), Branch.io або Adjust.
Universal Links на iOS
Потребує файл apple-app-site-association на сервері:
// https://yourdomain.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["TEAMID.com.company.app"],
"components": [
{ "/": "/product/*", "comment": "Сторінки товарів" },
{ "/": "/profile/*" },
{ "/": "/order/*" },
{ "/": "/promo/*", "?": { "ref": "?" } }
]
}
]
}
}
AASA повинен відданий з Content-Type: application/json, без редиректів, з кодом 200. Apple CDN кешує його агресивно — зміни вступають у силу з затримкою до 48 годин (Apple періодично обходить AASA у фоні). На iOS 16+ додано "mode": "developer" для прискорення оновлення AASA в debug.
У Info.plist — Associated Domains:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourdomain.com</string>
<string>applinks:www.yourdomain.com</string>
</array>
Обробка в 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 на Android
Аналог Universal Links. Файл assetlinks.json на сервері:
// 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 берется із keystore: keytool -list -v -keystore release.jks. Для debug та release — різні fingerprints, обидва потрібні в production assetlinks.json або використовуйте різні домени.
У 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" запускає верифікацію домена при встановленні. Перевірити верифікацію: adb shell pm get-app-links com.company.app. Якщо STATE_APPROVED — App Links працюють. Якщо STATE_NO_RESPONSE або STATE_FAILED_VERIFICATION — проблема з AASA або сертифікатом.
Частої проблеми на Android 12+: навіть при успішній верифікації користувач може вибрати "Відкривати в браузері" в системних налаштуваннях. Додаток не може це контролювати програмно.
Роутер на клієнті
Централізований роутер — обов'язково. Ніяких if (url.contains("product")) розкиданих по коду.
// 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 -> {
// Невідомий маршрут — відкрити в браузері
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 в браузер
}
}
}
Deferred Deep Links через Branch.io
Firebase Dynamic Links офіційно deprecated з серпня 2025. Альтернативи: Branch.io, Adjust, AppsFlyer, Airbridge — всі надають SDK для iOS та Android.
Branch.io SDK відстежує клік до встановлення та відновлює параметри після першого запуску:
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 викликається при кожному запуску — SDK визначає, був це organic запуск чи перехід по посиланню.
Тестування
Верифікація App Links: adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.company.app
Верифікація Universal Links на симуляторі — неможлива. Тільки на фізичному пристрої через xcrun simctl openurl booted "https://yourdomain.com/product/123" для симулятора iOS 14+, або через Notes.app → натиск по посиланню.
Перевірка AASA: curl -I https://yourdomain.com/.well-known/apple-app-site-association — потребує 200, без редиректів.
Apple надає валідатор: https://app-site-association.cdn-apple.com/a/v1/yourdomain.com — як Apple кешує ваш AASA файл.
Типові помилки
Redirect на CDN ломає App Links. Якщо yourdomain.com редиректить на www.yourdomain.com, а AASA на одному — верифікація падає. AASA потрібен на обох доменах.
Неправильний Content-Type. AASA з Content-Type: text/html — Apple не приймає. Тільки application/json.
Deep link при cold start vs warm start. На Android Intent приходить в onCreate при cold start та в onNewIntent при warm start. Якщо обробляйте тільки в onCreate — deep link при warm start ігнорується. Обробляйте в обох місцях.
Навігація до ініціалізації. Роутер намагається навігувати до того, як NavController готовий. Потрібна черга pending deep links, яка обробляється після ініціалізації навігації.
Реалізація Universal Links + App Links + роутер + deferred deep links: 2-4 тижні. Вартість розраховується індивідуально.







