[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-aggiornamenti-e-recensioni-in-app-in-flutter-force-update-in-app-update-e-in-app-review":3,"comments-article-aggiornamenti-e-recensioni-in-app-in-flutter-force-update-in-app-update-e-in-app-review":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},111,"Aggiornamenti e recensioni in-app in Flutter: force update, in_app_update e in_app_review","aggiornamenti-e-recensioni-in-app-in-flutter-force-update-in-app-update-e-in-app-review","Come obbligare (o suggerire) l'aggiornamento di un'app Flutter e chiedere una recensione nel momento giusto: confronto versioni, in_app_update su Android, fallback su iOS e throttling con shared_preferences.","## Perché serve una strategia di aggiornamento\n\nQuando un'app Flutter è in produzione, non tutti gli utenti aggiornano. Le statistiche degli store mostrano che una quota non trascurabile di installazioni resta ferma a versioni vecchie anche per mesi: significa bug già risolti che continuano a generare crash, API di backend che devi mantenere retrocompatibili all'infinito e feature nuove che nessuno vede.\n\nLa soluzione è costruire un piccolo **update gate**: all'avvio l'app chiede al backend qual è la versione minima supportata e quella consigliata, confronta con la versione installata e decide se mostrare un blocco (force update), un suggerimento (soft update) o nulla.\n\nNello stesso flusso conviene ragionare anche sul **prompt di recensione**: chiederla nel momento sbagliato abbassa il rating, chiederla dopo un'azione di successo lo alza. Vediamo entrambi gli aspetti con codice pronto all'uso.\n\n## I pacchetti necessari\n\n```yaml\ndependencies:\n  package_info_plus: ^8.0.0     # versione e build number installati\n  in_app_update: ^4.2.3         # solo Android: Play In-App Updates\n  in_app_review: ^2.0.9         # prompt recensione nativo iOS\u002FAndroid\n  url_launcher: ^6.3.0          # fallback verso lo store\n  shared_preferences: ^2.3.0    # throttling del prompt\n```\n\nSu iOS non esiste un'API equivalente a Play In-App Updates: l'unica strada supportata è mostrare una schermata nostra e aprire l'App Store.\n\n## Confrontare le versioni nel modo giusto\n\nIl primo errore classico è confrontare le versioni come stringhe: `\"1.10.0\"` risulterebbe minore di `\"1.9.0\"`. Serve un confronto per componenti numeriche.\n\n```dart\n\u002F\u002F\u002F Ritorna \u003C 0 se a è precedente a b, 0 se uguali, > 0 se successiva.\nint compareVersions(String a, String b) {\n  List\u003Cint> parse(String v) => v\n      .split('+')\n      .first \u002F\u002F ignora il build number \"1.2.3+45\"\n      .split('.')\n      .map((p) => int.tryParse(p.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0)\n      .toList();\n\n  final pa = parse(a);\n  final pb = parse(b);\n  final len = pa.length > pb.length ? pa.length : pb.length;\n\n  for (var i = 0; i \u003C len; i++) {\n    final x = i \u003C pa.length ? pa[i] : 0;\n    final y = i \u003C pb.length ? pb[i] : 0;\n    if (x != y) return x.compareTo(y);\n  }\n  return 0;\n}\n```\n\nRecuperare la versione installata è immediato con `package_info_plus`:\n\n```dart\nimport 'package:package_info_plus\u002Fpackage_info_plus.dart';\n\nfinal info = await PackageInfo.fromPlatform();\nfinal currentVersion = info.version;     \u002F\u002F es. \"1.4.2\"\nfinal buildNumber = info.buildNumber;    \u002F\u002F es. \"142\"\n```\n\n## Il modello di policy lato server\n\nL'endpoint (o un parametro di Firebase Remote Config, o un JSON su CDN) può restituire qualcosa del genere:\n\n```json\n{\n  \"minSupportedVersion\": \"1.3.0\",\n  \"latestVersion\": \"1.6.0\",\n  \"storeUrlIos\": \"https:\u002F\u002Fapps.apple.com\u002Fapp\u002Fid1234567890\",\n  \"storeUrlAndroid\": \"https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.example.app\",\n  \"message\": \"Abbiamo risolto un problema nella sincronizzazione offline.\"\n}\n```\n\nModelliamo il risultato con un `enum` e una piccola classe:\n\n```dart\nenum UpdateStatus { none, optional, required }\n\nclass UpdatePolicy {\n  const UpdatePolicy({\n    required this.minSupportedVersion,\n    required this.latestVersion,\n    required this.storeUrl,\n    this.message,\n  });\n\n  final String minSupportedVersion;\n  final String latestVersion;\n  final String storeUrl;\n  final String? message;\n\n  UpdateStatus evaluate(String installedVersion) {\n    if (compareVersions(installedVersion, minSupportedVersion) \u003C 0) {\n      return UpdateStatus.required;\n    }\n    if (compareVersions(installedVersion, latestVersion) \u003C 0) {\n      return UpdateStatus.optional;\n    }\n    return UpdateStatus.none;\n  }\n}\n```\n\nSeparare `minSupportedVersion` da `latestVersion` è la chiave: la prima è una **regola di compatibilità** (tipicamente legata al contratto delle API), la seconda è solo un invito.\n\n## Android: Play In-App Updates\n\nSu Android il Play Store offre due modalità native, entrambe esposte dal pacchetto `in_app_update`:\n\n- **Flexible**: l'aggiornamento viene scaricato in background, l'utente continua a usare l'app e poi conferma il riavvio.\n- **Immediate**: schermata a tutto schermo bloccante gestita da Google Play, ideale per i force update.\n\n```dart\nimport 'dart:io';\nimport 'package:in_app_update\u002Fin_app_update.dart';\n\nFuture\u003Cbool> tryPlayUpdate({required bool immediate}) async {\n  if (!Platform.isAndroid) return false;\n  try {\n    final info = await InAppUpdate.checkForUpdate();\n    if (info.updateAvailability != UpdateAvailability.updateAvailable) {\n      return false;\n    }\n\n    if (immediate && info.immediateUpdateAllowed) {\n      await InAppUpdate.performImmediateUpdate();\n      return true;\n    }\n\n    if (!immediate && info.flexibleUpdateAllowed) {\n      await InAppUpdate.startFlexibleUpdate();\n      await InAppUpdate.completeFlexibleUpdate();\n      return true;\n    }\n  } catch (_) {\n    \u002F\u002F Nessun Play Store, build di debug, sideload: si passa al fallback.\n  }\n  return false;\n}\n```\n\nAttenzione: `checkForUpdate()` funziona solo su build installate **dal Play Store** (anche tramite canale di test interno). In debug lancia sempre un'eccezione, quindi il fallback verso `url_launcher` è obbligatorio.\n\n## iOS e fallback: aprire lo store\n\n```dart\nimport 'package:url_launcher\u002Furl_launcher.dart';\n\nFuture\u003Cvoid> openStore(String url) async {\n  final uri = Uri.parse(url);\n  if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {\n    throw Exception('Impossibile aprire lo store');\n  }\n}\n```\n\nSe non vuoi mantenere `latestVersion` a mano, su iOS puoi interrogare l'endpoint pubblico di lookup di iTunes (`https:\u002F\u002Fitunes.apple.com\u002Flookup?bundleId=...`) e leggere il campo `version`. È comodo, ma ha una propagazione della cache di qualche ora: per i force update critici resta più affidabile una configurazione lato server.\n\n## L'UpdateGate: bloccare l'app quando serve\n\nUn widget che avvolge l'app e mostra una schermata non dismissibile in caso di versione non supportata:\n\n```dart\nclass UpdateGate extends StatefulWidget {\n  const UpdateGate({super.key, required this.child, required this.loadPolicy});\n\n  final Widget child;\n  final Future\u003CUpdatePolicy> Function() loadPolicy;\n\n  @override\n  State\u003CUpdateGate> createState() => _UpdateGateState();\n}\n\nclass _UpdateGateState extends State\u003CUpdateGate> {\n  UpdateStatus _status = UpdateStatus.none;\n  UpdatePolicy? _policy;\n\n  @override\n  void initState() {\n    super.initState();\n    _check();\n  }\n\n  Future\u003Cvoid> _check() async {\n    try {\n      final policy = await widget.loadPolicy();\n      final info = await PackageInfo.fromPlatform();\n      final status = policy.evaluate(info.version);\n\n      if (status == UpdateStatus.required) {\n        final handled = await tryPlayUpdate(immediate: true);\n        if (handled) return; \u002F\u002F Play gestisce tutto\n      }\n      if (!mounted) return;\n      setState(() {\n        _policy = policy;\n        _status = status;\n      });\n    } catch (_) {\n      \u002F\u002F In caso di errore di rete NON bloccare mai l'utente.\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_status == UpdateStatus.required && _policy != null) {\n      return _ForceUpdateScreen(policy: _policy!);\n    }\n    return widget.child;\n  }\n}\n```\n\nLa schermata bloccante usa `PopScope` per impedire il back su Android:\n\n```dart\nclass _ForceUpdateScreen extends StatelessWidget {\n  const _ForceUpdateScreen({required this.policy});\n  final UpdatePolicy policy;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopScope(\n      canPop: false,\n      child: Scaffold(\n        body: Center(\n          child: Padding(\n            padding: const EdgeInsets.all(24),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const Icon(Icons.system_update, size: 64),\n                const SizedBox(height: 16),\n                Text('Aggiornamento necessario',\n                    style: Theme.of(context).textTheme.headlineSmall),\n                const SizedBox(height: 8),\n                Text(\n                  policy.message ??\n                      'Questa versione non è più supportata. Aggiorna per continuare.',\n                  textAlign: TextAlign.center,\n                ),\n                const SizedBox(height: 24),\n                FilledButton(\n                  onPressed: () => openStore(policy.storeUrl),\n                  child: const Text('Aggiorna ora'),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\nPer l'aggiornamento **opzionale** evita la schermata a tutto schermo: un `MaterialBanner` o una `SnackBar` con azione \"Aggiorna\" è molto meno invasiva e non fa crollare le sessioni.\n\n## Chiedere la recensione nel momento giusto\n\nSia iOS (`SKStoreReviewController`) sia Android (Play In-App Review) espongono un prompt nativo che **non lascia l'app**. Il pacchetto `in_app_review` li unifica:\n\n```dart\nimport 'package:in_app_review\u002Fin_app_review.dart';\n\nfinal inAppReview = InAppReview.instance;\n\nif (await inAppReview.isAvailable()) {\n  await inAppReview.requestReview();\n}\n```\n\nDue punti fondamentali, spesso ignorati:\n\n- Il sistema **può decidere di non mostrare nulla**. iOS limita il prompt a 3 volte per anno per utente, Play applica quote analoghe. Non esiste alcun callback che ti dica se l'utente ha davvero recensito.\n- Per questo motivo **non va mai collegato a un pulsante \"Lascia una recensione\"**: per quello esiste `openStoreListing()`, che porta esplicitamente alla scheda dello store.\n\n### Un ReviewService con throttling\n\nDato che le quote di sistema sono silenziose, conviene sprecarle il meno possibile: chiedi la recensione solo a utenti \"felici\", cioè dopo N azioni di successo, mai al primo avvio e non più di una volta ogni X mesi.\n\n```dart\nimport 'package:shared_preferences\u002Fshared_preferences.dart';\n\nclass ReviewService {\n  static const _kEvents = 'review_success_events';\n  static const _kLastAsk = 'review_last_ask_ms';\n  static const _minEvents = 5;\n  static const _cooldown = Duration(days: 120);\n\n  final InAppReview _review = InAppReview.instance;\n\n  \u002F\u002F\u002F Da chiamare dopo un'azione positiva (ordine completato,\n  \u002F\u002F\u002F allenamento salvato, export riuscito...).\n  Future\u003Cvoid> registerSuccess() async {\n    final prefs = await SharedPreferences.getInstance();\n    final events = (prefs.getInt(_kEvents) ?? 0) + 1;\n    await prefs.setInt(_kEvents, events);\n\n    if (events \u003C _minEvents) return;\n\n    final lastAsk = prefs.getInt(_kLastAsk);\n    if (lastAsk != null) {\n      final elapsed = DateTime.now().millisecondsSinceEpoch - lastAsk;\n      if (elapsed \u003C _cooldown.inMilliseconds) return;\n    }\n\n    if (!await _review.isAvailable()) return;\n\n    await _review.requestReview();\n    await prefs.setInt(_kLastAsk, DateTime.now().millisecondsSinceEpoch);\n    await prefs.setInt(_kEvents, 0);\n  }\n\n  Future\u003Cvoid> openStorePage() =>\n      _review.openStoreListing(appStoreId: '1234567890');\n}\n```\n\nUn ulteriore livello, usato dalle app con rating alti, è il **pre-prompt**: prima chiedi \"Ti stai trovando bene?\" con una UI tua; se la risposta è positiva mostri `requestReview()`, se è negativa apri il form di feedback interno. Così le quote di sistema vengono spese solo sugli utenti soddisfatti.\n\n## Errori comuni da evitare\n\n- **Bloccare l'app se la chiamata alla policy fallisce.** Se il backend è giù, l'app deve partire comunque: il `try\u002Fcatch` che ignora l'errore è intenzionale.\n- **Fare il check a ogni rebuild.** Esegui il controllo in `initState` o all'`AppLifecycleState.resumed` con un debounce di qualche ora, non a ogni navigazione.\n- **Mostrare il prompt di recensione all'avvio o dopo un errore.** È il modo più veloce per raccogliere una stella.\n- **Non testare il flusso su build reali.** Sia `in_app_update` sia il prompt di recensione richiedono un'installazione proveniente dallo store (track interno o TestFlight). In debug il codice deve degradare senza crash.\n- **Dimenticare il build number.** Se pubblichi hotfix con la stessa `version` ma build diverse, includi il build number nella policy di confronto.\n\n## Testare la logica\n\nLa parte più preziosa da testare è il confronto di versioni, che è codice Dart puro:\n\n```dart\ntest('valuta correttamente lo stato di aggiornamento', () {\n  const policy = UpdatePolicy(\n    minSupportedVersion: '1.3.0',\n    latestVersion: '1.6.0',\n    storeUrl: 'https:\u002F\u002Fexample.com',\n  );\n\n  expect(policy.evaluate('1.2.9'), UpdateStatus.required);\n  expect(policy.evaluate('1.3.0'), UpdateStatus.optional);\n  expect(policy.evaluate('1.10.0'), UpdateStatus.none);\n});\n```\n\n## Conclusioni\n\nAggiornamenti e recensioni sono due funzionalità \"di contorno\" che incidono direttamente su crash rate, costi di manutenzione del backend e visibilità negli store. Con poche decine di righe di Dart puoi avere un update gate robusto — force update lato server, Play In-App Updates su Android, fallback verso lo store su iOS — e un prompt di recensione che rispetta l'utente e le quote di sistema. La regola d'oro resta una: blocca solo quando è davvero necessario e chiedi la recensione solo quando l'utente ha appena vissuto un successo.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fdaf5e59a-25dc-4659-87a3-6f498c9132c9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762329372047-5aadd8e1bd37?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA1NjgwOTl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Zulfugar Karimov","https:\u002F\u002Funsplash.com\u002F@zulfugarkarimov","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fyandex-browser-app-shown-on-a-smartphone-screen-pKUVUXBQe9A",null,"published","2026-09-28T04:01:40+00:00","Force update e recensioni in-app in Flutter","Guida pratica per gestire force update e recensioni in Flutter: confronto versioni, in_app_update su Android, fallback iOS e prompt con in_app_review.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316908650]