Tüm Yazılar
KategoriFlutter
Okuma Süresi
14 dk
Yayın Tarihi
2025-12-17
Kelime Sayısı
2.824kelime

Kahveni hazırla - bu içerikli bir makale!

Flutter Deep Link: Universal Links ve App Links Rehberi

Özet

Flutter deep link kurulumu: iOS Universal Links (AASA + entitlement), Android App Links (assetlinks.json + intent filter) ve Flutter route eşlemesini resmi kaynaklarla adım adım kuruyoruz.

  • Custom scheme, Android App Links ve iOS Universal Links üç ayrı mekanizmadır; production'da güvenilir yönlendirme yalnızca App Links/Universal Links ile sağlanır.
  • iOS'ta applinks:<domain> entitlement'ı + /.well-known/apple-app-site-association dosyası; dosya 301/302 yönlendirme içeremez ve swcutil dl/swcutil verify ile test edilir.
  • Android'de android:autoVerify="true" intent-filter'ı + /.well-known/assetlinks.json dosyası; doğrulama asenkron çalışır ve en az 20 saniye beklenmesi gerekir.
  • Flutter tarafında hem initialRoute hem pushRoute/RouteInformationParser dinlenmeli — iOS ve Android cold start'ta farklı sinyal sırası kullanır.
Flutter Deep Link: Universal Links ve App Links Rehberi

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

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.com
3 
4# Belirli bir path'in entitlement'la eşleşip eşleşmediğini test et
5sudo swcutil verify -d example.com -j /path/to/aasa.json -u https://example.com/detay/42

Cihazda 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.keystore

Play 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.app

Baş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 @override
3 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.

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.json ile Content-Type: application/json ve 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, verified bekle.
  • 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/RouteInformationParser tetiklendiğ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

Üç 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.

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/RouteInformationParser kullanı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

Etiketler

#flutter#deep-link#universal-links#app-links#ios#android#routing
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

İlgili İçerik