[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-monetizzare-unapp-flutter-con-admob-banner-adattivi-interstitial-e-consenso-gdpr":3,"comments-article-monetizzare-unapp-flutter-con-admob-banner-adattivi-interstitial-e-consenso-gdpr":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},117,"Monetizzare un'app Flutter con AdMob: banner adattivi, interstitial e consenso GDPR","monetizzare-unapp-flutter-con-admob-banner-adattivi-interstitial-e-consenso-gdpr","Guida pratica a google_mobile_ads: configurazione nativa, banner adattivi, interstitial e rewarded, gestione del consenso con UMP e ATT su iOS, più le best practice per non rovinare l'esperienza utente.","## Perché parlare di pubblicità in un'app Flutter\n\nGli acquisti in-app e gli abbonamenti non sono l'unica via per monetizzare: per molte app di utility, giochi casual o contenuti editoriali la pubblicità resta il modello più immediato. Su Flutter lo standard di fatto è **`google_mobile_ads`**, il plugin ufficiale di Google che espone l'SDK AdMob per Android e iOS.\n\nIl punto delicato non è tecnico ma **normativo e di UX**: senza una corretta gestione del consenso (GDPR in Europa, App Tracking Transparency su iOS) le richieste di annunci vengono rifiutate o monetizzano pochissimo; senza una strategia di frequenza, gli interstitial fanno disinstallare l'app. Vediamo come fare le cose per bene.\n\n## Installazione e configurazione nativa\n\nAggiungiamo la dipendenza:\n\n```yaml\ndependencies:\n  google_mobile_ads: ^6.0.0\n```\n\n### Android\n\nIn `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`, dentro il tag `\u003Capplication>`:\n\n```xml\n\u003Cmeta-data\n    android:name=\"com.google.android.gms.ads.APPLICATION_ID\"\n    android:value=\"ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY\"\u002F>\n```\n\nAttenzione: l'**App ID** usa la tilde (`~`), mentre gli **Ad Unit ID** usano lo slash (`\u002F`). È l'errore più comune e provoca un crash all'avvio.\n\nIl `minSdkVersion` richiesto dall'SDK recente è 23 o superiore.\n\n### iOS\n\nIn `ios\u002FRunner\u002FInfo.plist`:\n\n```xml\n\u003Ckey>GADApplicationIdentifier\u003C\u002Fkey>\n\u003Cstring>ca-app-pub-XXXXXXXXXXXXXXXX~ZZZZZZZZZZ\u003C\u002Fstring>\n\u003Ckey>NSUserTrackingUsageDescription\u003C\u002Fkey>\n\u003Cstring>Usiamo questo identificativo per mostrarti annunci più pertinenti.\u003C\u002Fstring>\n```\n\nVanno inoltre aggiunte le voci `SKAdNetworkItems` fornite nella documentazione AdMob (sono decine, servono per l'attribuzione delle installazioni).\n\n## Inizializzazione\n\nL'inizializzazione va fatta una sola volta, ma **dopo** aver raccolto il consenso: altrimenti l'SDK potrebbe richiedere annunci personalizzati senza averne il diritto.\n\n```dart\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  runApp(const MyApp());\n}\n```\n\nL'inizializzazione vera e propria la incapsuliamo in un servizio:\n\n```dart\nclass AdsService {\n  AdsService._();\n  static final instance = AdsService._();\n\n  bool _initialized = false;\n\n  Future\u003Cvoid> initialize() async {\n    if (_initialized) return;\n    await MobileAds.instance.initialize();\n    \u002F\u002F Dispositivi di test: evitano click non validi durante lo sviluppo\n    await MobileAds.instance.updateRequestConfiguration(\n      RequestConfiguration(\n        testDeviceIds: const ['IL_TUO_DEVICE_ID'],\n      ),\n    );\n    _initialized = true;\n  }\n}\n```\n\n## Gestione del consenso con UMP\n\nIl plugin include le **User Messaging Platform API**, che mostrano il form di consenso richiesto da Google per il traffico SEE\u002FUK e gestiscono anche il *Consent Mode*.\n\n```dart\nFuture\u003Cvoid> requestConsent() async {\n  final params = ConsentRequestParameters();\n\n  final completer = Completer\u003Cvoid>();\n  ConsentInformation.instance.requestConsentInfoUpdate(\n    params,\n    () async {\n      \u002F\u002F Mostra il form solo se necessario (area SEE\u002FUK, consenso scaduto...)\n      ConsentForm.loadAndShowConsentFormIfRequired((FormError? error) {\n        if (error != null) {\n          debugPrint('Errore form consenso: ${error.message}');\n        }\n        completer.complete();\n      });\n    },\n    (FormError error) {\n      debugPrint('Errore consenso: ${error.message}');\n      completer.complete();\n    },\n  );\n\n  await completer.future;\n\n  if (await ConsentInformation.instance.canRequestAds()) {\n    await AdsService.instance.initialize();\n  }\n}\n```\n\nDue accorgimenti importanti:\n\n- Se l'utente rifiuta il tracciamento, `canRequestAds()` può comunque restituire `true`: significa che si possono servire **annunci non personalizzati**. Non bloccare la monetizzazione in quel caso.\n- Nelle impostazioni dell'app vale la pena esporre un pulsante \"Gestisci privacy\" che richiami `ConsentForm.showPrivacyOptionsForm`, obbligatorio quando `getPrivacyOptionsRequirementStatus()` è `required`.\n\nSu iOS, dopo UMP, va mostrato il prompt ATT (con il pacchetto `app_tracking_transparency`), rispettando l'ordine: prima il form Google, poi ATT.\n\n## Banner adattivi\n\nIl formato banner classico a 320x50 è ormai superato: Google raccomanda gli **anchored adaptive banner**, che calcolano l'altezza ottimale in base alla larghezza dello schermo e rendono molto meglio.\n\n```dart\nclass AdaptiveBanner extends StatefulWidget {\n  const AdaptiveBanner({super.key});\n\n  @override\n  State\u003CAdaptiveBanner> createState() => _AdaptiveBannerState();\n}\n\nclass _AdaptiveBannerState extends State\u003CAdaptiveBanner> {\n  BannerAd? _ad;\n  bool _loaded = false;\n\n  static String get _unitId => Platform.isAndroid\n      ? 'ca-app-pub-3940256099942544\u002F9214589741' \u002F\u002F test Android\n      : 'ca-app-pub-3940256099942544\u002F2435281174'; \u002F\u002F test iOS\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _loadAd();\n  }\n\n  Future\u003Cvoid> _loadAd() async {\n    if (_ad != null) return;\n\n    final width = MediaQuery.sizeOf(context).width.truncate();\n    final size = await AdSize\n        .getCurrentOrientationAnchoredAdaptiveBannerAdSize(width);\n    if (size == null || !mounted) return;\n\n    final ad = BannerAd(\n      adUnitId: _unitId,\n      size: size,\n      request: const AdRequest(),\n      listener: BannerAdListener(\n        onAdLoaded: (_) => setState(() => _loaded = true),\n        onAdFailedToLoad: (ad, error) {\n          ad.dispose();\n          debugPrint('Banner fallito: ${error.code} ${error.message}');\n        },\n      ),\n    );\n\n    _ad = ad;\n    await ad.load();\n  }\n\n  @override\n  void dispose() {\n    _ad?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (!_loaded || _ad == null) return const SizedBox.shrink();\n    return SizedBox(\n      width: _ad!.size.width.toDouble(),\n      height: _ad!.size.height.toDouble(),\n      child: AdWidget(ad: _ad!),\n    );\n  }\n}\n```\n\nRegole d'oro per i banner:\n\n- **Un `AdWidget` per ogni `BannerAd`**: riutilizzare la stessa istanza in due punti dell'albero provoca un'eccezione.\n- Chiamare sempre `dispose()` sull'oggetto `Ad`, non solo sul widget: altrimenti si accumulano view native e memoria.\n- Riservare lo spazio nel layout (ad esempio un `SizedBox` con l'altezza attesa) per evitare *layout shift* quando l'annuncio arriva.\n\n## Interstitial: precaricamento e frequency capping\n\nUn interstitial va **caricato in anticipo** e mostrato in un momento naturale (fine di un livello, ritorno da un flusso completato), mai al primo avvio né durante un'interazione.\n\n```dart\nclass InterstitialManager {\n  InterstitialAd? _ad;\n  int _retry = 0;\n  DateTime? _lastShown;\n\n  static const _minInterval = Duration(minutes: 3);\n\n  void load() {\n    InterstitialAd.load(\n      adUnitId: 'ca-app-pub-3940256099942544\u002F1033173712',\n      request: const AdRequest(),\n      adLoadCallback: InterstitialAdLoadCallback(\n        onAdLoaded: (ad) {\n          _ad = ad;\n          _retry = 0;\n        },\n        onAdFailedToLoad: (error) {\n          _ad = null;\n          _retry++;\n          if (_retry \u003C= 3) {\n            Future.delayed(Duration(seconds: 2 * _retry), load);\n          }\n        },\n      ),\n    );\n  }\n\n  Future\u003Cvoid> showIfReady() async {\n    final ad = _ad;\n    if (ad == null) return;\n\n    final now = DateTime.now();\n    if (_lastShown != null && now.difference(_lastShown!) \u003C _minInterval) {\n      return;\n    }\n\n    ad.fullScreenContentCallback = FullScreenContentCallback(\n      onAdDismissedFullScreenContent: (ad) {\n        ad.dispose();\n        _ad = null;\n        load(); \u002F\u002F precarica il prossimo\n      },\n      onAdFailedToShowFullScreenContent: (ad, error) {\n        ad.dispose();\n        _ad = null;\n        load();\n      },\n    );\n\n    _lastShown = now;\n    await ad.show();\n  }\n}\n```\n\nIl `_minInterval` è un **frequency cap lato client**: AdMob ne offre uno lato dashboard, ma averlo anche nel codice evita sorprese e permette di differenziarlo per tipo di utente.\n\n## Rewarded ad: il formato con il miglior rapporto UX\u002Fricavi\n\nGli annunci con premio sono opt-in: l'utente sceglie di guardarli in cambio di qualcosa (una vita extra, un export senza watermark, crediti). Sono quelli meglio tollerati.\n\n```dart\nFuture\u003Cvoid> showRewarded(VoidCallback onReward) async {\n  RewardedAd.load(\n    adUnitId: 'ca-app-pub-3940256099942544\u002F5224354917',\n    request: const AdRequest(),\n    rewardedAdLoadCallback: RewardedAdLoadCallback(\n      onAdLoaded: (ad) {\n        ad.fullScreenContentCallback = FullScreenContentCallback(\n          onAdDismissedFullScreenContent: (ad) => ad.dispose(),\n          onAdFailedToShowFullScreenContent: (ad, _) => ad.dispose(),\n        );\n        ad.show(\n          onUserEarnedReward: (ad, reward) {\n            debugPrint('Premio: ${reward.amount} ${reward.type}');\n            onReward();\n          },\n        );\n      },\n      onAdFailedToLoad: (error) => debugPrint('Rewarded KO: $error'),\n    ),\n  );\n}\n```\n\n**Mai** assegnare il premio in `onAdDismissedFullScreenContent`: l'unico callback affidabile è `onUserEarnedReward`. Per premi di valore economico reale (valuta di gioco acquistabile) conviene attivare le **Server-Side Verification** e accreditare il premio dal backend.\n\n## App Open Ad e ciclo di vita\n\nL'App Open Ad si mostra al rientro in foreground. Richiede attenzione: va evitato al primissimo avvio (dove c'è la splash) e non deve comparire se l'utente sta tornando da un altro annuncio o da un flusso di pagamento. Si combina con `AppLifecycleListener` per intercettare il passaggio a `resumed`, mantenendo un flag che indica se un altro annuncio full-screen è in corso.\n\nUn altro dettaglio spesso dimenticato: gli annunci caricati **scadono dopo circa 4 ore**. Conviene salvare il timestamp di caricamento e ricaricare se l'annuncio è troppo vecchio.\n\n## Testare senza farsi bannare\n\n- In sviluppo usare **sempre** gli ad unit di test ufficiali di Google (quelli con prefisso `ca-app-pub-3940256099942544`).\n- Sui dispositivi reali registrare il *test device ID* (compare nei log dell'SDK al primo caricamento) e passarlo a `RequestConfiguration`.\n- Cliccare sui propri annunci di produzione è il modo più rapido per vedersi sospendere l'account AdMob.\n\nUn pattern comodo è centralizzare gli ID:\n\n```dart\nabstract final class AdIds {\n  static const _isTest = bool.fromEnvironment('ADS_TEST', defaultValue: true);\n\n  static String get banner => _isTest\n      ? 'ca-app-pub-3940256099942544\u002F9214589741'\n      : const String.fromEnvironment('BANNER_UNIT_ID');\n}\n```\n\nCosì con `--dart-define` si passa agli ID reali solo nelle build di release, idealmente collegandolo ai flavor dell'app.\n\n## Best practice finali\n\n1. **Non mostrare annunci agli utenti paganti.** Se esiste una versione premium, il controllo sull'abbonamento deve precedere ogni richiesta di annuncio.\n2. **Misura il ritorno reale.** Gli annunci full-screen alzano l'eCPM ma possono abbassare retention e sessioni: monitora entrambe le metriche prima di aumentare la frequenza.\n3. **Attenzione alle policy sui posizionamenti.** Banner appiccicati a pulsanti, annunci sotto scroll infiniti senza separazione o annunci su schermate di caricamento sono violazioni comuni.\n4. **Gestisci gli errori in silenzio.** Un annuncio che non carica non deve mai rompere la UI: in caso di fallimento il widget semplicemente non occupa spazio.\n5. **Conformità per i minori.** Se l'app rientra in Families Policy, imposta `tagForChildDirectedTreatment` nella `RequestConfiguration` e limita i formati.\n\nIntegrare AdMob in Flutter è tecnicamente semplice; la differenza tra un'app che monetizza e una che viene disinstallata sta tutta nella **disciplina sui posizionamenti** e in una gestione del consenso fatta a regola d'arte.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F93440a2a-971a-4d0b-8db1-d0ceeea9e2f7.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1477013743164-ffc3a5e556da?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTEwODY0ODF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Edho Pratama","https:\u002F\u002Funsplash.com\u002F@edhogilangpratama","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fsmartphone-showing-google-site-yeB9jDmHm6M",null,"published","2026-10-04T04:01:22+00:00","AdMob in Flutter: banner, interstitial e consenso GDPR","Guida a google_mobile_ads in Flutter: configurazione, banner adattivi, interstitial, rewarded, consenso UMP\u002FGDPR, ATT su iOS e best practice di monetizzazione.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316907615]