Реализация 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 (AASA) на сервере:
// 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 → tap по ссылке.
Проверка 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 недели. Стоимость рассчитывается индивидуально.







