Deep Linking .NET MAUI 2026: Universal & App Links
Panduan lengkap deep linking .NET MAUI: konfigurasi Android App Links (assetlinks.json), iOS Universal Links (AASA), routing Shell, dan migrasi dari Firebase Dynamic Links yang sudah dihentikan. Termasuk trik debugging dari produksi.
Deep linking di .NET MAUI adalah mekanisme yang memungkinkan URL http/https membuka halaman spesifik di dalam aplikasi native Android dan iOS, menggunakan App Links di Android (via assetlinks.json) dan Universal Links di iOS (via apple-app-site-association). Sejak .NET 9, MAUI menyederhanakan pemetaan URI ke rute Shell, tetapi konfigurasi platform-native tetap wajib. Apple dan Google memverifikasi kepemilikan domain sebelum mengarahkan tautan langsung ke aplikasi Anda. Artikel ini membedah setiap file konfigurasi, kode C# yang dibutuhkan, dan trik debugging yang tidak Anda temukan di dokumentasi resmi.
App Links Android memerlukan assetlinks.json di /.well-known/ dan intent filter android:autoVerify="true" di MainActivity. Verifikasi menggunakan fingerprint SHA-256 dari signing key produksi.
Universal Links iOS memerlukan file apple-app-site-association tanpa ekstensi, disajikan sebagai application/json tanpa redirect, dan capability Associated Domains dengan prefix applinks:.
Firebase Dynamic Links resmi mati pada 25 Agustus 2025, jadi semua aplikasi wajib beralih ke App Links/Universal Links atau alternatif pihak ketiga seperti Branch atau AppsFlyer.
.NET MAUI menangani deep link melalui override OnAppLinkRequestReceived di App.xaml.cs, kemudian mem-parsing URI dan memanggil Shell.Current.GoToAsync dengan parameter query.
Testing deep link tidak sama seperti klik link biasa. Di Android gunakan adb shell am start dan di iOS gunakan xcrun simctl openurl agar mem-bypass cache verifikasi Safari.
Custom URL scheme (myapp://) tetap berguna sebagai fallback OAuth callback, tetapi tidak lagi direkomendasikan sebagai skema utama karena tidak bisa diverifikasi kepemilikan domain-nya.
Apa itu deep linking di .NET MAUI?
Deep linking adalah proses membuka layar tertentu di dalam aplikasi mobile lewat sebuah URL, alih-alih hanya membuka layar beranda. Di ekosistem .NET MAUI, ada tiga jenis tautan yang saling berbeda perilaku sistem operasinya: App Links untuk Android (URL https:// yang diverifikasi Google Play), Universal Links untuk iOS (URL https:// yang diverifikasi Apple), dan Custom URL Scheme seperti myapp://profile/42 yang tidak memerlukan domain sama sekali.
Yang membuat topik ini menyakitkan adalah kenyataan bahwa MAUI cuma mengurus lapisan tipis di atas API native. Anda tetap harus mengedit Info.plist, mengelola capability Associated Domains di Xcode, meng-host file di root domain HTTPS Anda, dan menghitung SHA-256 dari sertifikat penandatanganan release. Saya sudah mengirim tiga aplikasi produksi dengan alur ini di MAUI, dan setiap kali ada satu langkah kecil (biasanya redirect di CDN) yang membuat verifikasi gagal secara diam-diam. Sebelum menulis kode C#, pahami dulu bahwa yang menentukan apakah link Anda membuka aplikasi bukanlah kode Anda, melainkan konfigurasi server dan manifest.
Di .NET 9 dan .NET 10, tim MAUI merapikan Microsoft.Maui.ApplicationModel.AppActions dan menambahkan callback OnAppLinkRequestReceived di kelas App Anda. API ini berlaku sama untuk App Links dan Universal Links, cukup subscribe sekali dan Anda menerima objek Uri lintas platform.
Universal Links vs App Links vs Custom URL Scheme: apa bedanya?
Sebelum menulis satu baris kode, tentukan dulu jenis tautan yang Anda butuhkan. Ketiga jenis ini punya trade-off berbeda pada verifikasi, UX pengguna, dan dukungan browser. Tabel berikut merangkum apa yang penting untuk aplikasi produksi.
Aspek
App Links (Android)
Universal Links (iOS)
Custom URL Scheme
Format URL
https://domain.com/path
https://domain.com/path
myapp://path
Wajib domain HTTPS?
Ya, dengan file assetlinks.json
Ya, dengan file apple-app-site-association
Tidak
Verifikasi kepemilikan
Otomatis oleh Play/Android saat install
Otomatis oleh iOS saat install
Tidak ada, bisa di-hijack aplikasi lain
Fallback jika app tidak ada
Buka di browser (URL asli)
Buka di Safari (URL asli)
Error atau "no app can handle this URL"
Dukungan OS minimum
Android 6.0 (API 23)
iOS 9+
Semua versi
Cocok untuk OAuth callback?
Ya, tetapi butuh setup domain
Ya, tetapi butuh setup domain
Ya, pilihan default Microsoft Identity
Kelemahan utama
Butuh SHA-256 signing key produksi yang benar
Cache AASA 8 jam pada iOS 14+, sulit debug
Rawan bentrok dengan app lain
Aturan praktisnya di 2026: gunakan App Links dan Universal Links sebagai jalur utama untuk deep link marketing, email, dan sharing. Sisakan Custom URL Scheme untuk OAuth 2.0 redirect, pola yang direkomendasikan Microsoft Identity Web dan yang saya bahas di panduan autentikasi dan keamanan .NET MAUI.
Android App Links diverifikasi Google saat aplikasi dipasang. Sistem mengunduh file https://yourdomain.com/.well-known/assetlinks.json, memeriksa SHA-256 signing key di dalamnya, dan hanya kemudian menandai domain Anda sebagai "trusted". Kalau verifikasi gagal, Android akan menampilkan disambiguation dialog dan user diminta memilih browser atau aplikasi Anda setiap kali. Honestly, ini pengalaman UX yang sangat jelek jadi hindari dengan segala cara.
1. Ambil SHA-256 dari signing key produksi
Ini bukan debug key. Signing key produksi Anda adalah keystore yang digunakan untuk build Play Store release. Jalankan:
keytool -list -v -keystore /path/to/release.keystore -alias my_alias
# atau untuk Play App Signing (rekomendasi Google):
# Ambil SHA-256 dari Play Console -> Setup -> App integrity -> App signing key certificate
Kolom yang Anda butuhkan adalah SHA256:, 32 pasang hex dipisah titik dua.
2. Buat file assetlinks.json
Host file ini persis di https://yourdomain.com/.well-known/assetlinks.json, disajikan sebagai application/json, TANPA redirect (bahkan HTTPS→HTTPS), dan status HTTP 200:
AutoVerify = true adalah kunci. Tanpa ini, Android tidak akan pernah mengunduh assetlinks.json dan Anda akan selamanya melihat disambiguation dialog. Verifikasi ulang dengan adb shell pm get-app-links com.mobiletechlead.demo, status "verified" berarti sukses.
iOS Universal Links secara konseptual mirip App Links, perbedaannya di detail. Yang bikin frustasi: iOS meng-cache file AASA sampai 8 jam (iOS 14+) atau bahkan hingga aplikasi di-reinstall. Bug pada file AASA Anda hari ini baru terlihat besok pagi.
1. Buat file apple-app-site-association
Host file tanpa ekstensi (bukan .json) di https://yourdomain.com/.well-known/apple-app-site-association, disajikan sebagai application/json, tanpa redirect, dan HTTPS yang valid (self-signed cert ditolak):
Pastikan Anda memiliki App ID di Apple Developer portal dengan capability "Associated Domains" enabled. Kalau tidak, Xcode akan menolak provisioning profile dengan pesan yang tidak informatif.
3. Mode "alternate mode" untuk debugging
Setelah menghabiskan berapa sore mengulang siklus Delete App → Reboot → Install, saya sangat merekomendasikan menggunakan mode ?mode=developer di entitlement selama development. Dokumentasi Apple Supporting Associated Domains menjelaskan bagaimana developer mode mem-bypass cache. Sangat berguna, tetapi jangan lupa cabut sebelum submit ke App Store, atau reviewer akan bounce build Anda.
Menangkap deep link di kode MAUI
Setelah kedua platform Anda setup, kode C#-nya justru relatif ringkas. .NET MAUI memaparkan event OnAppLinkRequestReceived di class App, dan event ini dipanggil untuk App Links Android maupun Universal Links iOS. Berikut pattern lengkap yang saya gunakan di produksi:
// File: App.xaml.cs
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
protected override async void OnAppLinkRequestReceived(Uri uri)
{
base.OnAppLinkRequestReceived(uri);
// Contoh URI: https://mobiletechlead.com/app/product/42?ref=email
if (uri.Host.EndsWith("mobiletechlead.com", StringComparison.OrdinalIgnoreCase))
{
var segments = uri.AbsolutePath.Trim('/').Split('/');
if (segments.Length >= 2 && segments[0] == "app")
{
var route = segments[1] switch
{
"product" when segments.Length >= 3 => $"//products/details?id={segments[2]}",
"promo" => $"//promo?{uri.Query.TrimStart('?')}",
_ => "//home"
};
await Shell.Current.GoToAsync(route);
}
}
}
}
Beberapa hal yang tidak terlihat dari kode tetapi penting:
Cold start vs warm start: kalau aplikasi belum berjalan, iOS/Android akan meluncurkan aplikasi dan callback dipanggil setelah App selesai konstruksi. Pastikan Shell Anda sudah ter-initialize sebelum memanggil GoToAsync.
Threading: callback dipanggil di UI thread, aman untuk navigasi, tetapi hindari operasi I/O blocking di sini.
Parameter aman: selalu validasi panjang segments. URL malformed dari serangan phishing bisa membuat aplikasi Anda crash kalau Anda mengasumsikan struktur.
Routing Shell & passing parameter dari deep link
.NET MAUI Shell mendukung query string style parameter yang diterjemahkan menjadi properti pada halaman tujuan. Kombinasikan dengan [QueryProperty] attribute agar clean, seperti yang saya jelaskan di arsitektur .NET MAUI dengan Shell Navigation.
[QueryProperty(nameof(ProductId), "id")]
[QueryProperty(nameof(Referrer), "ref")]
public partial class ProductDetailsViewModel : ObservableObject
{
[ObservableProperty]
private string _productId;
[ObservableProperty]
private string _referrer;
partial void OnProductIdChanged(string value)
{
// Load produk saat parameter tiba dari deep link
_ = LoadProductAsync(value);
}
}
Pola ini memisahkan parsing URI (di App.xaml.cs) dari domain logic (di ViewModel), sehingga Anda bisa unit-test rute mapping tanpa perlu menjalankan aplikasi.
Cara test deep link di iOS & Android tanpa email/SMS
Menguji deep link dengan mengetik URL di browser sering tidak berhasil. Safari punya cache tersendiri, dan Chrome Android akan menampilkan intent picker. Cara "resmi" yang saya pakai setiap hari:
Android via adb
# Simulasi user click Universal Link
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://mobiletechlead.com/app/product/42?ref=test" \
com.mobiletechlead.demo
# Cek status verifikasi App Links
adb shell pm get-app-links com.mobiletechlead.demo
# Trigger ulang verifikasi App Links (Android 12+)
adb shell pm verify-app-links --re-verify com.mobiletechlead.demo
Trik lama yang masih terbaik. Buka aplikasi Notes iOS di device Anda, ketik URL, tap link. Karena Notes bukan aplikasi Anda, iOS akan menghormati Universal Link dan membuka aplikasi Anda. Chrome/Safari tidak selalu berperilaku sama. Saya bahkan pernah hit bug di mana Safari mengembalikan HTTP 200 dari AASA tetapi tetap menolak buka aplikasi. Beralih ke Notes membuktikan bahwa file AASA saya sebenarnya sudah benar.
Migrasi dari Firebase Dynamic Links yang dihentikan
Google resmi mematikan Firebase Dynamic Links pada 25 Agustus 2025. Semua tautan yang di-generate lewat FDL sekarang hanya mengembalikan HTTP 404 atau redirect ke halaman deprecation. Kalau aplikasi MAUI atau Xamarin.Forms Anda masih pakai FDL, Anda punya tiga jalur migrasi:
Migrasi penuh ke App Links + Universal Links, rekomendasi Google sendiri di FAQ deprecation FDL. Ini yang saya rekomendasikan kalau Anda tidak butuh deferred deep linking (kasus di mana user tap link, lalu install app dari store, dan setelah pertama kali dibuka aplikasi langsung navigasi ke konten yang tepat).
Branch.io, AppsFlyer, atau Adjust. Ketiganya menawarkan deferred deep linking dan attribution. Berbayar, tetapi tidak perlu Anda maintain infrastruktur redirect sendiri.
Bangun sendiri: redirect endpoint di server Anda yang membaca User-Agent, mengarahkan ke Play Store/App Store kalau app belum terinstall, dan mem-preserve context via cookie/fingerprint agar diambil saat pertama kali aplikasi launch.
Untuk aplikasi produksi yang saya dampingi migrasi, jalur 1 cukup untuk 80% use case. Deferred deep linking sering di-overengineer. Kalau UTM tracking Anda sudah ada di email/marketing, App Links normal dengan query param sudah menangkap semua yang Anda butuhkan.
Troubleshooting: kenapa deep link saya tidak buka aplikasi?
Setelah puluhan sesi debugging, saya menyusun checklist urutan yang paling sering menjadi biang kerok. Jalani dari atas ke bawah.
Android: App Links tidak trigger
Cek autoVerify=true di intent filter. Kalau tidak ada, Android tidak akan pernah mendownload assetlinks.json.
Cek SHA-256, apakah Anda menggunakan fingerprint dari Play Signing certificate (bukan upload key)? Ambil dari Play Console → App Integrity.
Cek respons server: curl -I https://yourdomain.com/.well-known/assetlinks.json harus HTTP 200, Content-Type application/json, dan tidak ada 301/302 di antaranya. Detail spesifikasi ada di dokumentasi Android App Links resmi.
Cek verifikasi device: adb shell pm get-app-links <package>, status harus "verified". Kalau "unverified", biasanya cause di poin 2 atau 3.
Cek CDN: Cloudflare/Fastly sering meng-inject cache headers atau redirect HTTPS yang membuat Google verifier fail. Bypass CDN untuk path /.well-known/.
iOS: Universal Links tidak trigger
Uninstall dan reinstall aplikasi. iOS meng-cache AASA saat first install dan hanya me-refresh setelah beberapa jam.
Cek AASA dari cache Apple: curl https://app-site-association.cdn-apple.com/a/v1/yourdomain.com. Kalau Apple tidak melihat file Anda, device pun tidak akan pernah melihatnya.
Cek Team ID dan Bundle ID di AASA, harus persis TEAMID.bundleid, case-sensitive. Team ID ada di membership page Apple Developer.
Cek capability Associated Domains: buka file .entitlements, pastikan prefix applinks: ada dan domain-nya persis (tanpa https://).
Redirect terlarang: file AASA tidak boleh mengalami redirect satu kali pun. Bahkan HTTP → HTTPS redirect akan membuat iOS menolak verifikasi.
Untuk observability produksi, log kombinasi uri.Scheme, uri.Host, dan uri.PathAndQuery setiap kali OnAppLinkRequestReceived ter-trigger. Kalau Anda sudah menggunakan setup GitHub Actions dari panduan CI/CD .NET MAUI, tambahkan smoke test yang meng-invoke xcrun simctl openurl di post-build simulator boot. Regresi deep link biasanya masuk lewat perubahan routing yang tidak sengaja mem-break URI parsing.
Pertanyaan yang sering diajukan
Apakah saya masih perlu Custom URL Scheme kalau sudah pakai App Links dan Universal Links?
Ya, untuk kasus OAuth 2.0 redirect. Microsoft Identity Client (MSAL) dan Google Sign-In tetap menggunakan custom scheme sebagai callback default karena tidak membutuhkan verifikasi domain. Untuk marketing link, sharing, dan email, cukup App Links/Universal Links.
Bagaimana cara mendukung banyak domain sekaligus di satu aplikasi?
Di Android, tambahkan beberapa IntentFilter attribute (satu per domain) atau gunakan DataHosts array. Di iOS, tambahkan multiple entries di applinks: array Associated Domains. Setiap domain memerlukan assetlinks.json atau apple-app-site-association sendiri di root domain masing-masing.
Apakah deep link bekerja di .NET MAUI Blazor Hybrid?
Ya, OnAppLinkRequestReceived tetap dipanggil di App class. Anda kemudian bisa memanggil NavigationManager.NavigateTo di dalam komponen Blazor untuk berpindah rute. Yang perlu diperhatikan adalah race condition: pastikan BlazorWebView sudah selesai load sebelum navigasi diminta.
Bisakah deep link membawa parameter sensitif seperti token?
Sebaiknya tidak. URL Universal Link/App Link tetap dilog oleh sistem OS, dan pada Android, disambiguation dialog bisa mengekspos URL ke aplikasi lain. Gunakan short-lived exchange code yang di-tukarkan di server side, bukan token langsung. Prinsip yang sama berlaku untuk OAuth PKCE flow.
Kenapa deep link saya bekerja di iOS 16 tetapi tidak di iOS 17+?
Sejak iOS 17, Apple memperketat validasi components matcher di AASA. Beberapa pola wildcard yang dulu diterima sekarang gagal secara diam-diam. Jalankan file Anda lewat Apple AASA Validator dan pastikan setiap komponen menggunakan sintaks baru dengan key "/" dan "?" yang eksplisit, bukan singkatan lama.
Panduan lengkap membangun pipeline CI/CD .NET MAUI dengan GitHub Actions: build Android AAB dan iOS IPA, code signing, publish ke Play Store dan TestFlight, plus strategi versioning dan optimasi biaya runner macOS.
Panduan lengkap migrasi aplikasi Xamarin.Forms ke .NET MAUI 9: dari .NET Upgrade Assistant, port custom renderer ke handler, hingga validasi build per platform dengan estimasi waktu realistis.