Flutter'da deep link kurulumu genellikle "linke tıkladım, uygulama açılmadı" sorunuyla başlar ve çoğu zaman kök neden kodda değil, platform tarafındaki doğrulama dosyalarındadır. Bu rehberde iOS'ta Universal Links, Android'de App Links ve ikisinin ortak atası olan custom URL scheme'i birbirinden ayırıyor, her birinin sunucu tarafı gereksinimlerini ve Flutter'ın route eşleme mekanizmasını gerçek konfigürasyon dosyalarıyla adım adım kuruyoruz.
💡 Pro Tip: Deep link'i test etmeden önce mutlaka DevTools'un Deep Links doğrulayıcısını çalıştır — hem Android hem iOS tarafındaki eksik dosya/imza sorunlarını uygulamayı derlemeden önce yakalar.
İçindekiler
- Üç ayrı şey: custom scheme, App Links, Universal Links
- iOS: entitlement + apple-app-site-association + sunucu kuralları
- Android: assetlinks.json + intent filter + imza parmak izi
- Flutter tarafında route eşleme ve başlangıç link'ini yakalamak
- DevTools deep link doğrulayıcısı
- Cold start vs warm start farkı
- Sık yapılan hatalar ve teşhisi
- Analitik ve attribution ile ilişkisi
- Test kontrol listesi
- SSS
- Flutter'da deep link nasıl kurulur?
- Universal Links neden çalışmıyor?
- apple-app-site-association dosyası nereye konur?
- Flutter'da deep link'ler nasıl test edilir?
- Android'de doğrulama neden 20 saniye sürüyor?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Üç ayrı şey: custom scheme, App Links, Universal Links
Deep linking konusunda kafa karışıklığının büyük kısmı üç farklı mekanizmanın tek bir kavrammış gibi konuşulmasından kaynaklanıyor.
Custom URL scheme (myapp://detay/42 gibi) işletim sistemi seviyesinde hiçbir doğrulama gerektirmez; sadece Info.plist/AndroidManifest.xml içinde şemayı tanımlarsın ve herhangi bir uygulama aynı şemayı kaydedebilir. Bu yüzden production'da tek başına güvenilir bir yönlendirme mekanizması değildir — hem çakışma riski taşır hem de tarayıcıda tıklanan bir https:// linkini doğrudan yakalayamaz.
Android App Links, siteyle uygulama arasında doğrulanmış bir bağ kurarak deep link'i, kullanıcıya "hangi uygulamayla açılsın" diyalogunu göstermeden doğrudan uygulamada açar. Resmi tanıma göre: "Android App Links is an enhanced deep linking capability that verifies deep links to your own website by establishing a trusted association between your app and your website." Doğrulanmamış klasik deep link'ler ise sistemin disambiguation dialog'una tabidir. App Links, Google servisleri bulunan Android 6.0 ve üzeri cihazlarda desteklenir (kaynak: developer.android.com/training/app-links).
iOS Universal Links aynı işi Apple tarafında yapar: https:// linkleri, sunucudaki bir doğrulama dosyasıyla eşleşiyorsa Safari'de açılmadan doğrudan uygulamaya yönlendirilir. İkisinin de ortak noktası şu: doğrulama dosyası sunucuda barınır, uygulama tarafında ise ilgili entitlement/manifest girdisi bu dosyaya işaret eder. Ben genelde custom scheme'i tamamen atmıyorum; geliştirme sırasında xcrun simctl openurl veya adb shell am start -a android.intent.action.VIEW -d "myapp://detay/42" ile hızlıca tetiklenebildiği için sunucu tarafındaki AASA/assetlinks.json yayına alınmadan önce Flutter tarafındaki route eşleme kodunu test etmek için kullanışlı bir fallback oluyor. Production'da kullanıcıya gönderilen linkler her zaman https:// olmalı; custom scheme yalnızca geliştirme/test aşamasında ya da push notification payload'u gibi kapalı, kontrollü kanallarda tercih edilir.
Aşağıdaki iki bölüm bu doğrulama dosyalarının birebir şemasını gösteriyor.
iOS: entitlement + apple-app-site-association + sunucu kuralları
iOS tarafında üç parça birbirine kilitlenir: Xcode'daki Associated Domains entitlement'ı, sunucudaki AASA dosyası ve AASA'yı sunan sunucunun HTTP davranışı.
Entitlement formatı Apple'ın resmi TN3155 notunda şöyle geçiyor: uygulamanın Associated Domains capability'sinde applinks içermesi gerekiyor, "in the form of: applinks:<fully qualified domain>"; not ayrıca wildcard ile eşleşen "subdomains that are matched with the wildcard applinks:*.example.com" ifadesinden bahsediyor. Yani tek bir alan adı ya da wildcard ile birden fazla alt alan adı kapsanabilir.
xml
1<!-- ios/Runner/Runner.entitlements -->2<key>com.apple.developer.associated-domains</key>3<array>4 <string>applinks:example.com</string>5 <string>applinks:*.example.com</string>6</array>AASA dosyası sabit bir yolda barınmalı: TN3155'in ifadesiyle "an AASA file should be hosted at: https://example.com/.well-known/apple-app-site-association"; ayrıca not şunu ekliyor: "Each specific subdomain in your applinks should have its own matching AASA file path."
json
1{2 "applinks": {3 "details": [4 {5 "appIDs": ["TEAMID.com.example.app"],6 "components": [{ "/": "/detay/*", "comment": "Ürün detay sayfası" }]7 }8 ]9 }10}Sunucu tarafında en sık atlanan kural şu: AASA yanıtı 301/302 HTTP yönlendirmesi İÇEREMEZ. TN3155'in ifadesiyle: "If the response contains a 301 or 302 HTTP status code... is not supported." Bunun yerine dosya, applinks'e dahil edilen her alan adında ve alt alan adında ayrı ayrı barındırılmalı. Apple'ın CDN'i AASA dosyasını başarıyla önbelleğe alabilmek için "a domain that is available to all IP addresses and ranges, does not redirect, and is not blocked by access policies" koşulunu arar; hızlı test için ?mode=developer (alternate mode) ile CDN'i atlayıp dosyayı doğrudan alan adından çekebilirsin.
Doğrulama için iki komut işini görür:
bash
1# AASA indirme testi (Apple'ın gördüğü haliyle)2sudo swcutil dl -d example.com3 4# Belirli bir path'in entitlement'la eşleşip eşleşmediğini test et5sudo swcutil verify -d example.com -j /path/to/aasa.json -u https://example.com/detay/42Cihazda onay durumunu kontrol etmek için sysdiagnose alıp swcutil_show.txt dosyasında App ID'yi aramak gerekir — bu, Xcode konsolunda göremeyeceğin sistem seviyesi bir kayıttır.
Android: assetlinks.json + intent filter + imza parmak izi
Android tarafında doğrulama dosyası assetlinks.json adını taşır ve üç alan zorunludur: package_name (build.gradle'daki application ID), sha256_cert_fingerprints (imzalama sertifikasının SHA256 parmak izi — birden fazla değer desteklenir) ve relation: ["delegate_permission/common.handle_all_urls"].
json
1[2 {3 "relation": ["delegate_permission/common.handle_all_urls"],4 "target": {5 "namespace": "android_app",6 "package_name": "com.example.app",7 "sha256_cert_fingerprints": [8 "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"9 ]10 }11 }12]Parmak izi, imzalama anahtarından keytool ile üretilir:
bash
1keytool -list -v -keystore my-release-key.keystorePlay App Signing kullanıyorsan doğru JSON parçasını elle üretmene gerek yok — Play Console → Release → Setup → App signing altında hazır JSON snippet'i bulunur. assetlinks.json dosyası application/json content-type ile, HTTPS üzerinden ve yönlendirmesiz (301/302 yok) erişilebilir olmalı; birden fazla host domaini varsa dosya her domainde ayrı yayınlanmalı (kaynak: developer.android.com/training/app-links/configure-assetlinks).
Manifest tarafında android:autoVerify="true" bayrağı en az bir intent-filter'da olmalı:
xml
1<intent-filter android:autoVerify="true">2 <action android:name="android.intent.action.VIEW" />3 <category android:name="android.intent.category.DEFAULT" />4 <category android:name="android.intent.category.BROWSABLE" />5 <data android:scheme="https" android:host="example.com" />6</intent-filter>Android 6.0 (API 23) ve üzeri bir cihaza kurulum, sistemin URL'lerle ilişkili host'ları otomatik doğrulamasını tetikler; sistem yalnızca VIEW action + BROWSABLE/DEFAULT kategorileri + http/https şeması olan intent filter'ları inceler. Sistem her benzersiz host için dosyayı https://hostname/.well-known/assetlinks.json adresinden sorgular ve asenkron doğrulama süreci için en az 20 saniye beklemek gerekir — bu süre dolmadan test edip "çalışmıyor" sonucuna varmak yaygın bir yanlış teşhistir.
Doğrulama durumu şu komutla kontrol edilir:
bash
1adb shell pm get-app-links com.example.appBaşarılı domainler verified durumunda görünür; none, legacy_failure veya 1024+ gibi durumlar bir sorun olduğunu gösterir (kaynak: developer.android.com/training/app-links/verify-android-applinks).
Flutter tarafında route eşleme ve başlangıç link'ini yakalamak
Sunucu tarafı doğrulama tamam olduktan sonra iş Flutter'a düşer. Flutter, deep link ile açılan URL'i named routes (routes parametresi ya da onGenerateRoute) veya Router widget'ı üzerinden ekrana yansıtır. Resmi rehber artık named route'ları çoğu uygulama için önermiyor: "Named routes are no longer recommended for most applications." — bu yüzden yeni bir kurulumda Router/RouteInformationParser tabanlı bir yaklaşım tercih etmelisin.
dart
1class AppRouteInformationParser extends RouteInformationParser<AppRoutePath> {2 @override3 Future<AppRoutePath> parseRouteInformation(4 RouteInformation routeInformation,5 ) async {6 final uri = routeInformation.uri;7 if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'detay') {8 final id = int.tryParse(uri.pathSegments[1]);9 if (id != null) return AppRoutePath.detail(id);10 }11 return AppRoutePath.home();12 }13}Flutter'ın varsayılan deep-link işleyicisini kapatmak istersen (örneğin kendi native kodunla link'i işleyeceksen), iOS'ta Info.plist'te FlutterDeepLinkingEnabled değerini false yapman, Android'de ise AndroidManifest.xml'de flutter_deeplinking_enabled meta-data'sını false yapman gerekir. Flutter 3.27'den itibaren deep linking varsayılan olarak açık; 3.27 öncesi sürümlerde flutter_deeplinking_enabled meta-data'sını true değeriyle elle eklemek gerekiyordu (kaynak: docs.flutter.dev/cookbook/navigation/set-up-app-links).
Eski bir projeyi henüz Router widget'ına taşımadıysan, named routes ile de aynı sonucu elde edebilirsin — sadece dinamik path parametrelerini (/detay/42 gibi) onGenerateRoute içinde elle parse etmen gerekir:
dart
1MaterialApp(2 onGenerateRoute: (settings) {3 final uri = Uri.parse(settings.name ?? '/');4 if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'detay') {5 final id = int.tryParse(uri.pathSegments[1]);6 if (id != null) {7 return MaterialPageRoute(builder: (_) => DetailPage(id: id));8 }9 }10 return MaterialPageRoute(builder: (_) => const HomePage());11 },12)İki yaklaşım da aynı URL'i tüketir; fark, Router yaklaşımının tarayıcı geçmişi (web hedefinde) ve deklaratif navigasyon durumu ile daha iyi entegre olmasıdır. Yeni bir proje başlatıyorsan resmi rehberin önerdiği gibi doğrudan Router ile başlamak, ileride migrasyon maliyetinden kaçınmanı sağlar.
DevTools deep link doğrulayıcısı
Sunucu tarafındaki iki dosyayı (AASA ve assetlinks.json) elle satır satır kontrol etmek yerine, kod yazmadan önce kurulumu doğrulamanın en hızlı yolu DevTools'un Deep Links sekmesidir. Resmi tanıma göre bu araç, bir Flutter projesini içe aktararak website konfigürasyonundan manifest dosyalarına kadar deep-link kurulumundaki hataları tespit ediyor ve sorunları düzeltmek için talimatlar sunuyor şeklinde özetleniyor. 3.27 sürümünden itibaren doğrulayıcı hem Android hem iOS için çalışıyor — bu yazının yazıldığı 2025-12-17 itibarıyla (o sırada güncel stable sürüm 3.38.5'ti) bu özellik yaklaşık bir yıldır mevcuttu, yani her iki platformu da tek araçtan kontrol edebilirsin.
Pratikte akış şöyle: DevTools → Deep Links → proje kökünü seç → araç hem AASA/assetlinks.json dosyalarını canlı sunucudan çeker hem de Xcode/Gradle konfigürasyonunu okuyup eşleşmeyen alanları (yanlış Team ID, eksik intent-filter, yanlış paket adı) tek tek listeler.
Cold start vs warm start farkı
Deep link'in uygulamaya ulaştığı an, uygulamanın o anki durumuna göre farklı bir API'den geçer. Bu fark platformlar arasında da simetrik değildir:
Durum | Android (kapalıyken) | iOS (kapalıyken) | Her ikisi (açıkken) |
|---|---|---|---|
İlk sinyal | initialRoute doğrudan hedef path'i taşır (örn. /detay) | initialRoute önce / gelir | — |
İkinci sinyal | yok (tek seferde doğru path) | kısa süre sonra ayrı bir pushRoute çağrısıyla asıl link iletilir | pushRoute çağrılır |
Risk | düşük | başlangıç ekranı kısa süre yanlış görünebilir | düşük |
Bu platform farkının pratik sonucu şu: route-yakalama kodun hem initialRoute hem pushRoute (ya da Router kullanıyorsan RouteInformationParser) dinlemek zorunda. Yalnızca initialRoute'a güvenen bir implementasyon Android'de çalışır görünür ama iOS'ta cold start senaryosunda link'i kaçırır — bu, "iOS'ta bazen çalışmıyor" şikayetlerinin sık rastlanan bir kaynağıdır.
Sık yapılan hatalar ve teşhisi
Aşağıdaki tablo, yukarıdaki bölümlerde geçen resmi kurallara dayanan en sık rastlanan kurulum hatalarını ve doğrudan teşhis yolunu listeliyor:
Belirti | Kök neden | Teşhis komutu/yolu |
|---|---|---|
iOS'ta link Safari'de açılıyor, uygulama açılmıyor | AASA 301/302 yönlendirme arkasında | swcutil dl -d <domain> ile ham yanıtı kontrol et |
Android'de disambiguation dialog hâlâ çıkıyor | assetlinks.json yanlış content-type ile sunuluyor | curl -I ile Content-Type: application/json doğrula |
Doğrulama "beklemede" gibi kalıyor | 20 saniyelik asenkron pencere dolmadan test edilmiş | adb shell pm get-app-links ile tekrar sorgula |
assetlinks.json doğru ama doğrulama geçmiyor | sha256_cert_fingerprints debug keystore'dan alınmış | Play Console → App signing altındaki gerçek parmak izini kullan |
iOS cold start'ta yanlış ekran kısa süre görünüyor | Kod yalnızca initialRoute dinliyor, pushRoute yakalanmıyor | Cold/warm start tablosundaki her iki API'yi de dinle |
Analitik ve attribution ile ilişkisi
Deep link URL'indeki query parametreleri (kampanya kaynağı, referral kodu gibi) route eşlemesinden SONRA analytics event'ine geçirilmelidir — yani önce Router/onGenerateRoute URL'i parse eder, ardından uygulama bu parametreleri kendi analytics çağrısına iletir; bu sıralama, parse adımı geçerli bir sonuç üretmeden hiçbir analytics event'inin tetiklenmemesini garanti eder.
Kendi projende bu ayrımı yaparken pratik kural şu: route parse edilene kadar hiçbir analytics event'i gönderme. Aksi halde geçersiz ya da eksik bir deep link için de "başarılı açılış" event'i loglamış olursun, bu da funnel/attribution verini kirletir. Router/onGenerateRoute içindeki parse adımının sonucu (başarılı mı, hangi path mi) tek doğruluk kaynağı olmalı.
Test kontrol listesi
Kuruluma "bitti" demeden önce sırayla:
- iOS AASA erişimi:
swcutil dl -d <domain>ile dosyayı indir, 301/302 olmadığını doğrula. - iOS entitlement eşleşmesi:
swcutil verify -d <domain> -j aasa.json -u <test-url>ile hedef path'i doğrula. - Android assetlinks.json:
curl -I https://<domain>/.well-known/assetlinks.jsonileContent-Type: application/jsonve yönlendirme olmadığını kontrol et. - Android doğrulama durumu: kurulumdan en az 20 saniye sonra
adb shell pm get-app-links <package>çalıştır,verifiedbekle. - DevTools Deep Links: proje kökünü açıp hem Android hem iOS için "no issues found" gördüğünden emin ol.
- Cold start senaryosu: uygulamayı tamamen kapat, linke tıkla; hem Android hem iOS'ta doğru ekranın (iOS'ta kısa gecikmeyle de olsa) açıldığını gözlemle.
- Warm start senaryosu: uygulama açıkken linke tıkla,
pushRoute/RouteInformationParsertetiklendiğini doğrula.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Bu rehberdeki tüm komutları ve dosya yollarını tek bir kontrol listesinde topladım, böylece kurulumu yaparken sekmeler arasında dolaşmak yerine tek yere bakabilirsin. Liste hem iOS hem Android tarafını, hem dosya yollarını hem doğrulama komutlarını, hem de en sık atlanan bekleme süresini içeriyor — üstteki bölümlerin tamamına dağılmış pratik bilgiyi tek ekranda görmek isteyenler için.
SSS
Flutter'da deep link nasıl kurulur?
Üç parçayı ayrı ayrı kurman gerekir: iOS'ta Associated Domains entitlement'ı + /.well-known/apple-app-site-association dosyası, Android'de android:autoVerify="true" intent-filter'ı + /.well-known/assetlinks.json dosyası, ve Flutter tarafında Router/RouteInformationParser (ya da named routes) ile URL'i ekrana yansıtan kod. Kurulumu bitirdikten sonra DevTools'un Deep Links doğrulayıcısıyla her iki platformu da tek seferde kontrol edebilirsin.
Universal Links neden çalışmıyor?
En sık üç neden: AASA dosyası 301/302 yönlendirme arkasında (Apple'ın kuralına göre bu desteklenmiyor), entitlement'taki applinks:<domain> formatı alan adıyla eşleşmiyor, ya da CDN cache'i henüz güncellenmedi (entitlement'a ?mode=developer (alternate mode) ekleyerek Apple'ın CDN'ini atlayıp dosyayı doğrudan test edebilirsin). swcutil dl ve swcutil verify komutları bu üçünü de ayırt etmeni sağlar.
apple-app-site-association dosyası nereye konur?
Sunucunun kök dizininde /.well-known/apple-app-site-association yoluna, yönlendirmesiz ve HTTPS üzerinden erişilebilir şekilde konur. applinks altında birden fazla alt alan adı tanımlıysa, her alt alan adı için dosya o alt alan adında ayrıca barındırılmalıdır — tek bir dosyayı ana domainde tutup alt domainlerden yönlendirmek desteklenmez.
Flutter'da deep link'ler nasıl test edilir?
Sunucu tarafı için swcutil (iOS) ve curl -I + adb shell pm get-app-links (Android) komutlarıyla dosyaların doğru sunulduğunu doğrula. Uygulama tarafı için DevTools Deep Links doğrulayıcısını çalıştır, ardından hem cold start (uygulama tamamen kapalıyken linke tıklama) hem warm start (uygulama açıkken) senaryolarını elle test et — bu iki senaryo farklı Flutter API'lerinden geçtiği için ayrı ayrı doğrulanmalı.
Android'de doğrulama neden 20 saniye sürüyor?
Android, autoVerify="true" bayrağını gördüğünde ilgili host'lar için Digital Asset Links dosyasını arka planda asenkron olarak sorgular; bu süreç senkron değildir ve resmi rehber en az 20 saniye beklenmesini söyler. Bu süre dolmadan adb shell pm get-app-links ile kontrol edersen durumu henüz verified olarak göremeyebilirsin.
Güncelleme (Eylül 2026)
Bu yazı 2025-12-17 tarihinde Flutter 3.38.5 ile yazıldı. O tarihten bu yana kurulumun temel mekaniğinde (AASA şeması, assetlinks.json alanları, autoVerify akışı) bir kırılma olmadı; bir nokta yine de not edilmeye değer:
- Flutter stable 3.38.6 yayınlandı: Bu yazı 3.38.5 stable ile yazıldı; Flutter'ın resmi sürüm arşivine göre stable kanal 2026-01-08 tarihinde 3.38.6'ya ilerledi. Bu sürüm, yukarıdaki deep link kurulum adımlarında (AASA şeması, assetlinks.json alanları, autoVerify akışı,
Router/RouteInformationParserkullanımı) herhangi bir değişiklik getirmedi; yalnızca bir patch sürümü olarak not düşülüyor.
Sonuç
Deep link kurulumunun püf noktası kod değil, sunucudaki iki doğrulama dosyasının (AASA ve assetlinks.json) eksiksiz ve yönlendirmesiz sunulması. Flutter tarafında Router/RouteInformationParser ile hem initialRoute hem pushRoute dinlendiğinde cold/warm start farkı da devre dışı kalır. Kurulumu bitirdikten sonra DevTools'un doğrulayıcısıyla iki platformu da tek seferde kontrol etmek, prod'a çıkmadan önce en ucuz doğrulama adımıdır.
Flutter'ın navigasyon ve state yönetimi tarafını derinleştirmek istersen Flutter'da State Management: Riverpod Rehberi yazısına bakabilirsin; native köprü tarafı için Flutter iOS Platform Channel ve mimari tarafı için Flutter Clean Architecture yazıları bu rehberi tamamlıyor. Test kontrol listesini genişletmek istersen Flutter Testing: Kapsamlı Rehber yazısına, Dart tarafındaki güncel dil özellikleri için Dart 3: Yeni Özellikler yazısına göz atabilirsin. SwiftUI'da benzer bir navigasyon/deep-linking sorununu farklı bir eksenden (Coordinator pattern) ele alan SwiftUI'da Özel Navigasyon yazısı da bu konuya yakın bir bakış sunuyor.
Kaynaklar
- Android App Links — genel bakış — App Links'in disambiguation dialog'u nasıl atladığını ve Android 6.0+ desteğini anlatan resmi sayfa.
- Android — assetlinks.json yapılandırma —
package_name,sha256_cert_fingerprintsverelationalanlarının resmi şeması. - Android — App Links doğrulama —
autoVerify, 20 saniyelik asenkron doğrulama veadb shell pm get-app-linkskomutu. - Apple TN3155 — Universal Links hata ayıklama — entitlement formatı, AASA yönlendirme kısıtı,
swcutilkomutları. - Flutter — Deep linking rehberi — named routes/
Routereşlemesi ve cold/warm start davranış tablosu. - Flutter — App Links kurulum rehberi (cookbook) — 3.27 öncesi sürümlerde
flutter_deeplinking_enabledmeta-data'sının elle eklenmesi gerektiğini gösteren resmi kaynak. - Flutter DevTools — Deep Links doğrulayıcısı — aracın kapsamı ve 3.27'den beri iki platform desteği.
- Flutter sürüm bilgisi (releases feed) — stable kanal sürüm/tarih verisi (Güncelleme bölümü kaynağı).

