[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-deep-link-e-app-links-in-flutter-configurazione-avanzata-con-go-router":3,"comments-tutorial-deep-link-e-app-links-in-flutter-configurazione-avanzata-con-go-router":85},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},92,"Deep link e App Links in Flutter: configurazione avanzata con go_router","deep-link-e-app-links-in-flutter-configurazione-avanzata-con-go-router","Guida avanzata ai deep link in Flutter: App Links su Android, Universal Links su iOS, routing con go_router, gestione cold start con app_links, guardie di rotta e test da terminale.","I **deep link** trasformano un URL in un punto di ingresso diretto dentro la tua app: `https:\u002F\u002Fapp.example.com\u002Fprodotti\u002F42` non apre più il browser, ma la schermata di dettaglio del prodotto 42, con lo stack di navigazione corretto e il tasto indietro che funziona come l'utente si aspetta.\n\nIn questo tutorial **avanzato** affronteremo l'intera catena:\n\n- configurazione di `go_router` con rotte parametriche e gestione degli URL sconosciuti;\n- **Android App Links** verificati tramite `assetlinks.json` e firma dell'app;\n- **iOS Universal Links** con Associated Domains e `apple-app-site-association`;\n- gestione del **cold start** e dei link ricevuti ad app già aperta con il pacchetto `app_links`;\n- **guardie di rotta**: cosa succede se il link punta a un'area protetta e l'utente non è autenticato (pattern del *pending deep link*);\n- test reali da terminale con `adb` e `xcrun simctl`, più il debug della verifica dei domini.\n\n> Prerequisiti: conoscenza di Flutter e `go_router`, accesso a un dominio HTTPS su cui poter pubblicare file statici, Android Studio e (per iOS) Xcode con un Apple Developer Account.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F05438a57-938c-4e01-8cbf-47ceb16fe487.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1648134859182-98df6e93ef58?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODkxODc1MjJ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Team Nocoloco","https:\u002F\u002Funsplash.com\u002F@teamnocoloco","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-computer-screen-with-a-cell-phone-on-it-kpxYUeWRai4","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=5jbzVF6amUw",{"youtube_id":17,"duration_seconds":18,"published_at":19},"5jbzVF6amUw",183,"2026-09-12T08:59:07+00:00","advanced",55,"3.x","published","2026-09-12T04:32:02+00:00","Deep link e App Links in Flutter: guida avanzata","Configura deep link in Flutter: App Links Android, Universal Links iOS, go_router, cold start con app_links, guardie di rotta e test da terminale.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,57,64,71,78],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},612,"Progettare lo schema degli URL e configurare go_router","Prima di toccare i file nativi bisogna decidere **la mappa degli URL**. Un buon schema di deep link è stabile nel tempo, leggibile e speculare alla struttura del sito web: se il sito espone `\u002Fprodotti\u002F:id`, l'app deve rispondere allo stesso path.\n\nPunti chiave della configurazione:\n\n- `initialLocation` è usata solo quando **non** arriva un deep link: `go_router` legge automaticamente la route iniziale dalla piattaforma.\n- `errorBuilder` gestisce gli URL sconosciuti (link vecchi, typo, campagne mal configurate): mai lasciare una schermata bianca.\n- Le rotte annidate con `routes:` generano automaticamente lo **stack** corretto, quindi da `\u002Fprodotti\u002F42` il back porta a `\u002Fprodotti` e non fuori dall'app.\n\nAggiungi le dipendenze in `pubspec.yaml`:\n\n```yaml\ndependencies:\n  go_router: ^14.0.0\n  app_links: ^6.0.0\n```","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:go_router\u002Fgo_router.dart';\n\nfinal GlobalKey\u003CNavigatorState> rootNavigatorKey = GlobalKey\u003CNavigatorState>();\n\nfinal GoRouter appRouter = GoRouter(\n  navigatorKey: rootNavigatorKey,\n  initialLocation: '\u002F',\n  debugLogDiagnostics: true,\n  routes: \u003CRouteBase>[\n    GoRoute(\n      path: '\u002F',\n      name: 'home',\n      builder: (context, state) => const HomePage(),\n      routes: \u003CRouteBase>[\n        GoRoute(\n          path: 'prodotti',\n          name: 'prodotti',\n          builder: (context, state) => const ProdottiPage(),\n          routes: \u003CRouteBase>[\n            GoRoute(\n              path: ':id',\n              name: 'prodottoDettaglio',\n              builder: (context, state) {\n                final id = state.pathParameters['id']!;\n                \u002F\u002F Query string: \u002Fprodotti\u002F42?ref=newsletter\n                final ref = state.uri.queryParameters['ref'];\n                return ProdottoDettaglioPage(id: id, campagna: ref);\n              },\n            ),\n          ],\n        ),\n        GoRoute(\n          path: 'profilo',\n          name: 'profilo',\n          builder: (context, state) => const ProfiloPage(),\n        ),\n      ],\n    ),\n  ],\n  errorBuilder: (context, state) => LinkNonValidoPage(uri: state.uri),\n);\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp.router(\n      title: 'Deep Link Demo',\n      routerConfig: appRouter,\n    );\n  }\n}","dart","L'app si avvia sulla home; navigando manualmente a `\u002Fprodotti\u002F42` (ad esempio con `context.go`) viene mostrato il dettaglio e il back riporta alla lista prodotti.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},613,2,"Configurare Android App Links con verifica del dominio","Su Android un link `https:\u002F\u002F` apre l'app **senza chiedere nulla all'utente** solo se il dominio è verificato tramite Digital Asset Links. Servono due pezzi: l'intent filter con `android:autoVerify=\"true\"` e il file `assetlinks.json` pubblicato sul dominio.\n\n**1. Intent filter** in `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`, dentro l'`\u003Cactivity>` principale (`.MainActivity`). Aggiungi anche `\u003Cmeta-data android:name=\"flutter_deeplinking_enabled\" android:value=\"true\" \u002F>` per delegare il routing a Flutter\u002Fgo_router.\n\n**2. Ottieni l'impronta SHA-256** della chiave con cui firmi l'app (una per il debug keystore, una per la release, più quella di **Play App Signing** che trovi nella Play Console → Configurazione → Integrità dell'app):\n\n```bash\nkeytool -list -v -keystore ~\u002F.android\u002Fdebug.keystore \\\n  -alias androiddebugkey -storepass android -keypass android\n```\n\n**3. Pubblica** il file su `https:\u002F\u002Fapp.example.com\u002F.well-known\u002Fassetlinks.json`, servito con `Content-Type: application\u002Fjson`, **senza redirect** e raggiungibile pubblicamente (attenzione a Cloudflare\u002FWAF che bloccano user-agent sconosciuti).","\u003C!-- AndroidManifest.xml -->\n\u003Cactivity\n    android:name=\".MainActivity\"\n    android:exported=\"true\"\n    android:launchMode=\"singleTop\"\n    android:theme=\"@style\u002FLaunchTheme\">\n\n    \u003Cmeta-data android:name=\"flutter_deeplinking_enabled\" android:value=\"true\" \u002F>\n\n    \u003Cintent-filter>\n        \u003Caction android:name=\"android.intent.action.MAIN\" \u002F>\n        \u003Ccategory android:name=\"android.intent.category.LAUNCHER\" \u002F>\n    \u003C\u002Fintent-filter>\n\n    \u003C!-- App Link verificato -->\n    \u003Cintent-filter android:autoVerify=\"true\">\n        \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n        \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n        \u003Ccategory android:name=\"android.intent.category.BROWSABLE\" \u002F>\n        \u003Cdata android:scheme=\"https\" android:host=\"app.example.com\" \u002F>\n    \u003C\u002Fintent-filter>\n\n    \u003C!-- Fallback custom scheme: myapp:\u002F\u002Fprodotti\u002F42 -->\n    \u003Cintent-filter>\n        \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n        \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n        \u003Ccategory android:name=\"android.intent.category.BROWSABLE\" \u002F>\n        \u003Cdata android:scheme=\"myapp\" android:host=\"prodotti\" \u002F>\n    \u003C\u002Fintent-filter>\n\u003C\u002Factivity>\n\n\u003C!-- File da pubblicare su https:\u002F\u002Fapp.example.com\u002F.well-known\u002Fassetlinks.json -->\n[{\n  \"relation\": [\"delegate_permission\u002Fcommon.handle_all_urls\"],\n  \"target\": {\n    \"namespace\": \"android_app\",\n    \"package_name\": \"com.example.deeplinkdemo\",\n    \"sha256_cert_fingerprints\": [\n      \"AA:BB:CC:...:FF\",\n      \"11:22:33:...:99\"\n    ]\n  }\n}]","xml","Dopo l'installazione, `adb shell pm get-app-links com.example.deeplinkdemo` mostra il dominio con stato `verified`, e aprendo il link da Gmail o dalle Note si apre direttamente l'app.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":48,"expected_result":56,"demo_url":41,"video_url":41},614,3,"Configurare iOS Universal Links con Associated Domains","Su iOS il meccanismo equivalente sono gli **Universal Links**. Servono tre passaggi:\n\n**1. Capability Associated Domains**: in Xcode, target Runner → *Signing & Capabilities* → `+ Capability` → *Associated Domains*, e aggiungi `applinks:app.example.com`. Xcode crea\u002Faggiorna `ios\u002FRunner\u002FRunner.entitlements`. Il tuo App ID deve avere la capability abilitata nel Developer Portal (rigenera i profili di provisioning).\n\n**2. File `apple-app-site-association`** (senza estensione!) pubblicato su `https:\u002F\u002Fapp.example.com\u002F.well-known\u002Fapple-app-site-association`, servito come `application\u002Fjson`, **senza redirect** e senza firma. `appID` è `TEAMID.bundleIdentifier`.\n\n**3. Custom scheme di fallback** in `ios\u002FRunner\u002FInfo.plist` per i casi in cui l'Universal Link non è disponibile (es. link incollato nella barra indirizzi di Safari).\n\n> ⚠️ iOS scarica l'AASA tramite la CDN di Apple e la mette in cache. In sviluppo aggiungi `applinks:app.example.com?mode=developer` (iOS 14+ con la modalità sviluppatore attiva) per bypassare la cache, oppure disinstalla e reinstalla l'app.","\u003C!-- ios\u002FRunner\u002FRunner.entitlements -->\n\u003Ckey>com.apple.developer.associated-domains\u003C\u002Fkey>\n\u003Carray>\n  \u003Cstring>applinks:app.example.com\u003C\u002Fstring>\n\u003C\u002Farray>\n\n\u003C!-- https:\u002F\u002Fapp.example.com\u002F.well-known\u002Fapple-app-site-association -->\n{\n  \"applinks\": {\n    \"details\": [\n      {\n        \"appIDs\": [\"ABCDE12345.com.example.deeplinkdemo\"],\n        \"components\": [\n          { \"\u002F\": \"\u002Fprodotti\u002F*\", \"comment\": \"Dettaglio prodotto\" },\n          { \"\u002F\": \"\u002Fprofilo\", \"comment\": \"Area personale\" },\n          { \"\u002F\": \"\u002Fadmin\u002F*\", \"exclude\": true, \"comment\": \"Resta sul web\" }\n        ]\n      }\n    ]\n  }\n}\n\n\u003C!-- ios\u002FRunner\u002FInfo.plist : fallback custom scheme -->\n\u003Ckey>CFBundleURLTypes\u003C\u002Fkey>\n\u003Carray>\n  \u003Cdict>\n    \u003Ckey>CFBundleTypeRole\u003C\u002Fkey>\n    \u003Cstring>Editor\u003C\u002Fstring>\n    \u003Ckey>CFBundleURLName\u003C\u002Fkey>\n    \u003Cstring>com.example.deeplinkdemo\u003C\u002Fstring>\n    \u003Ckey>CFBundleURLSchemes\u003C\u002Fkey>\n    \u003Carray>\n      \u003Cstring>myapp\u003C\u002Fstring>\n    \u003C\u002Farray>\n  \u003C\u002Fdict>\n\u003C\u002Farray>","Su device fisico, aprendo `https:\u002F\u002Fapp.example.com\u002Fprodotti\u002F42` dalle Note o da Messaggi l'app si apre sulla schermata di dettaglio; da Safari compare il banner \"Apri nell'app\".",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":39,"expected_result":63,"demo_url":41,"video_url":41},615,4,"Intercettare i link a runtime con app_links (cold start e warm start)","Con `flutter_deeplinking_enabled` e gli intent filter corretti, `go_router` riceve già gli URL. Ma nelle app reali serve **controllo esplicito** per: normalizzare gli URL (host multipli, alias legacy, parametri UTM), fare logging\u002Fanalytics, o gestire schemi custom che non mappano 1:1 sui path.\n\nIl pacchetto `app_links` espone due API:\n\n- `getInitialLink()` → il link che ha **avviato** l'app (cold start), `null` se l'app è stata aperta normalmente;\n- `uriLinkStream` → lo stream dei link ricevuti mentre l'app è **già in esecuzione** (warm start).\n\nIl servizio qui sotto centralizza il parsing e usa il `router` per navigare. Nota l'uso di un flag `_coldStartHandled` per non processare due volte lo stesso link: su alcune piattaforme il link iniziale viene emesso **anche** dallo stream.\n\nSe gestisci tutto manualmente, imposta `flutter_deeplinking_enabled` a `false` su Android e implementa `application(_:continue:restorationHandler:)` solo se necessario: evita il doppio routing.","import 'dart:async';\nimport 'package:app_links\u002Fapp_links.dart';\nimport 'package:go_router\u002Fgo_router.dart';\n\nclass DeepLinkService {\n  DeepLinkService(this._router);\n\n  final GoRouter _router;\n  final AppLinks _appLinks = AppLinks();\n  StreamSubscription\u003CUri>? _sub;\n  bool _coldStartHandled = false;\n\n  \u002F\u002F\u002F Domini\u002Fhost accettati: difesa contro link contraffatti.\n  static const Set\u003CString> _hostConsentiti = {'app.example.com', 'example.com'};\n\n  Future\u003Cvoid> init() async {\n    final Uri? initial = await _appLinks.getInitialLink();\n    if (initial != null) {\n      _coldStartHandled = true;\n      _handle(initial, coldStart: true);\n    }\n\n    _sub = _appLinks.uriLinkStream.listen(\n      (uri) {\n        if (_coldStartHandled) {\n          _coldStartHandled = false; \u002F\u002F ignora il primo replay\n          return;\n        }\n        _handle(uri, coldStart: false);\n      },\n      onError: (Object e, StackTrace s) => print('Deep link error: $e'),\n    );\n  }\n\n  void _handle(Uri uri, {required bool coldStart}) {\n    final location = _normalizza(uri);\n    if (location == null) return;\n    \u002F\u002F Analytics: traccia la sorgente della campagna\n    final utm = uri.queryParameters['utm_source'];\n    print('Deep link ($coldStart ? cold : warm) -> $location (utm: $utm)');\n    _router.go(location);\n  }\n\n  \u002F\u002F\u002F Converte http(s) e custom scheme in una location di go_router.\n  String? _normalizza(Uri uri) {\n    if (uri.scheme == 'myapp') {\n      \u002F\u002F myapp:\u002F\u002Fprodotti\u002F42  -> host = prodotti, path = \u002F42\n      final segments = [uri.host, ...uri.pathSegments].where((s) => s.isNotEmpty);\n      return '\u002F${segments.join('\u002F')}${_query(uri)}';\n    }\n    if (uri.scheme == 'https' && _hostConsentiti.contains(uri.host)) {\n      \u002F\u002F Alias legacy: \u002Fp\u002F42 -> \u002Fprodotti\u002F42\n      if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'p') {\n        return '\u002Fprodotti\u002F${uri.pathSegments[1]}${_query(uri)}';\n      }\n      return '${uri.path}${_query(uri)}';\n    }\n    return null; \u002F\u002F schema o host non riconosciuto: ignora\n  }\n\n  String _query(Uri uri) =>\n      uri.query.isEmpty ? '' : '?${uri.query}';\n\n  void dispose() => _sub?.cancel();\n}","Con l'app chiusa o in background, aprire un link porta alla schermata corretta; nei log compare la riga `Deep link ... -> \u002Fprodotti\u002F42` una sola volta per link.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":39,"expected_result":70,"demo_url":41,"video_url":41},616,5,"Guardie di rotta e pending deep link per le aree protette","Il caso più delicato: il link punta a `\u002Fprofilo` ma l'utente non ha ancora effettuato il login. Il comportamento professionale è:\n\n1. il `redirect` di `go_router` intercetta la rotta protetta;\n2. l'URL richiesto viene **memorizzato** (pending deep link), passandolo come query param `from` o salvandolo in uno store;\n3. l'utente viene mandato al login;\n4. al termine dell'autenticazione il router riesegue il redirect e lo porta **esattamente** dove voleva andare.\n\nIl parametro `refreshListenable` fa sì che, al cambio di stato di autenticazione, `go_router` rivaluti il redirect senza che tu debba chiamare `go` a mano. Ricorda di codificare l'URL con `Uri.encodeComponent` e di **validare** il valore di `from` prima di reindirizzarci: un `from` arbitrario proveniente da un link esterno è un vettore di open redirect.","import 'package:flutter\u002Ffoundation.dart';\nimport 'package:go_router\u002Fgo_router.dart';\n\nclass AuthState extends ChangeNotifier {\n  bool _logged = false;\n  bool get isLogged => _logged;\n\n  void login() { _logged = true; notifyListeners(); }\n  void logout() { _logged = false; notifyListeners(); }\n}\n\nfinal auth = AuthState();\n\nconst Set\u003CString> _rottaProtetta = {'\u002Fprofilo', '\u002Fordini'};\n\nfinal router = GoRouter(\n  refreshListenable: auth,\n  routes: [\n    GoRoute(path: '\u002F', builder: (c, s) => const HomePage()),\n    GoRoute(path: '\u002Fprofilo', builder: (c, s) => const ProfiloPage()),\n    GoRoute(\n      path: '\u002Flogin',\n      builder: (c, s) => LoginPage(from: s.uri.queryParameters['from']),\n    ),\n  ],\n  redirect: (context, state) {\n    final loc = state.matchedLocation;\n    final protetta = _rottaProtetta.any(loc.startsWith);\n\n    if (protetta && !auth.isLogged) {\n      final target = Uri.encodeComponent(state.uri.toString());\n      return '\u002Flogin?from=$target';\n    }\n\n    if (loc == '\u002Flogin' && auth.isLogged) {\n      final from = state.uri.queryParameters['from'];\n      return _isSafe(from) ? from! : '\u002F';\n    }\n    return null; \u002F\u002F nessun redirect\n  },\n);\n\n\u002F\u002F\u002F Accetta solo path relativi interni: niente open redirect.\nbool _isSafe(String? value) {\n  if (value == null || value.isEmpty) return false;\n  final uri = Uri.tryParse(value);\n  return uri != null && !uri.hasScheme && !uri.hasAuthority && value.startsWith('\u002F');\n}","Aprendo `https:\u002F\u002Fapp.example.com\u002Fprofilo` da sloggati si arriva al login; dopo `auth.login()` l'app naviga automaticamente su `\u002Fprofilo` senza tap aggiuntivi.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":39,"expected_result":77,"demo_url":41,"video_url":41},617,6,"Testare i deep link da terminale su Android e iOS","Non affidarti solo ai tap manuali: i test da terminale isolano il problema (routing dell'app vs verifica del dominio).\n\n**Android** — simula un intent VIEW:\n\n```bash\n# App Link https\nadb shell am start -a android.intent.action.VIEW \\\n  -c android.intent.category.BROWSABLE \\\n  -d \"https:\u002F\u002Fapp.example.com\u002Fprodotti\u002F42?utm_source=test\"\n\n# Custom scheme\nadb shell am start -a android.intent.action.VIEW -d \"myapp:\u002F\u002Fprodotti\u002F42\"\n```\n\nVerifica lo stato della verifica dei domini (Android 12+):\n\n```bash\nadb shell pm get-app-links com.example.deeplinkdemo\n# Forza una nuova verifica\nadb shell pm verify-app-links --re-verify com.example.deeplinkdemo\n```\n\nSe leggi `legacy_failure` o `1024` (nessuna risposta), il problema è quasi sempre lato server: redirect, `Content-Type` errato, SHA-256 sbagliata.\n\n**iOS** — sul simulatore:\n\n```bash\nxcrun simctl openurl booted \"https:\u002F\u002Fapp.example.com\u002Fprodotti\u002F42\"\nxcrun simctl openurl booted \"myapp:\u002F\u002Fprodotti\u002F42\"\n```\n\nSu device fisico, gli Universal Links **non** si attivano digitando l'URL in Safari: usa Note, Messaggi o Mail.\n\n**Test automatizzato**: con `integration_test` puoi verificare il routing simulando il messaggio di piattaforma, senza dipendere dal sistema operativo.","\u002F\u002F test\u002Fdeep_link_router_test.dart\nimport 'package:flutter\u002Fwidgets.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:go_router\u002Fgo_router.dart';\n\nvoid main() {\n  testWidgets('Il deep link \u002Fprodotti\u002F42 apre il dettaglio', (tester) async {\n    final router = GoRouter(\n      initialLocation: '\u002Fprodotti\u002F42',\n      routes: [\n        GoRoute(path: '\u002F', builder: (c, s) => const Text('home')),\n        GoRoute(\n          path: '\u002Fprodotti\u002F:id',\n          builder: (c, s) => Text('dettaglio ${s.pathParameters['id']}'),\n        ),\n      ],\n    );\n\n    await tester.pumpWidget(MaterialApp.router(routerConfig: router));\n    await tester.pumpAndSettle();\n\n    expect(find.text('dettaglio 42'), findsOneWidget);\n  });\n\n  testWidgets('Un URL sconosciuto mostra la pagina di errore', (tester) async {\n    final router = GoRouter(\n      initialLocation: '\u002Frotta\u002Finesistente',\n      routes: [GoRoute(path: '\u002F', builder: (c, s) => const Text('home'))],\n      errorBuilder: (c, s) => const Text('link non valido'),\n    );\n\n    await tester.pumpWidget(MaterialApp.router(routerConfig: router));\n    await tester.pumpAndSettle();\n\n    expect(find.text('link non valido'), findsOneWidget);\n  });\n}","I comandi da terminale aprono l'app sulla schermata attesa e i due test passano con `flutter test`.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":39,"expected_result":84,"demo_url":41,"video_url":41},618,7,"Rifiniture: link condivisibili, fallback web e checklist di produzione","Ultimo miglio: rendere i link **generabili dall'app** e **resilienti** quando l'app non è installata.\n\n**Generare link di condivisione**: centralizza la costruzione degli URL in una sola classe, così web e app restano allineati.\n\n**Fallback web**: la pagina `https:\u002F\u002Fapp.example.com\u002Fprodotti\u002F42` deve esistere davvero e mostrare il contenuto. Se l'app non è installata, l'utente atterra sul sito (con eventuale smart banner iOS via `\u003Cmeta name=\"apple-itunes-app\">`). Evita le pagine \"scarica l'app\" cieche: perdi utenti e ranking SEO.\n\n### Checklist prima della release\n\n- [ ] `assetlinks.json` contiene **tutte** le SHA-256: debug, release upload key e **Play App Signing**.\n- [ ] Entrambi i file `.well-known` rispondono **200** senza redirect e con `Content-Type: application\u002Fjson`.\n- [ ] `apple-app-site-association` usa il TEAM ID di produzione e nessuna estensione `.json` nel nome file.\n- [ ] Ogni path pubblico dell'app ha un equivalente web funzionante.\n- [ ] Gli URL sconosciuti finiscono in `errorBuilder`, mai in schermata bianca.\n- [ ] Host e scheme vengono **validati** prima di navigare (nessun open redirect, nessun path da fonti non fidate).\n- [ ] I parametri `utm_*` vengono tracciati e poi rimossi dall'URL visibile.\n- [ ] Testato: app chiusa, app in background, app in foreground, utente sloggato.\n- [ ] Testato su Android 12+ (verifica automatica) e iOS con cache AASA ripulita.","class AppLinkBuilder {\n  static const String _host = 'app.example.com';\n\n  static Uri prodotto(String id, {String? campagna}) => Uri(\n        scheme: 'https',\n        host: _host,\n        pathSegments: ['prodotti', id],\n        queryParameters: campagna == null ? null : {'utm_source': campagna},\n      );\n\n  static Uri profilo() =>\n      Uri(scheme: 'https', host: _host, path: '\u002Fprofilo');\n}\n\n\u002F\u002F Uso con share_plus:\n\u002F\u002F await Share.share(AppLinkBuilder.prodotto('42', campagna: 'app').toString());\n\n\u002F\u002F Pulizia dei parametri di tracking dopo averli registrati\nextension UriTracking on Uri {\n  Uri senzaUtm() {\n    final params = Map\u003CString, String>.from(queryParameters)\n      ..removeWhere((k, _) => k.startsWith('utm_'));\n    return replace(queryParameters: params.isEmpty ? null : params);\n  }\n}","L'app genera link condivisibili coerenti con il sito, che si aprono nell'app se installata e sul web in caso contrario, con i parametri di campagna tracciati e poi ripuliti.",[],1789205499325]