[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-widget-per-la-home-screen-in-flutter-con-home-widget-android-e-ios":3,"comments-article-widget-per-la-home-screen-in-flutter-con-home-widget-android-e-ios":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},82,"Widget per la home screen in Flutter con home_widget: Android e iOS","widget-per-la-home-screen-in-flutter-con-home-widget-android-e-ios","Come portare i dati della tua app Flutter direttamente sulla home screen di Android e iOS usando il pacchetto home_widget: setup nativo, aggiornamento dei dati, deep link e refresh in background.","## Perché un widget per la home screen\n\nUn widget sulla home screen è uno dei pochi spazi in cui la tua app può \"esistere\" anche quando non viene aperta: meteo, saldo del conto, prossimo allenamento, ultima notizia, countdown. Su iOS (WidgetKit) e su Android (App Widgets) l'interfaccia del widget **non è disegnata da Flutter**: viene renderizzata dal sistema operativo con SwiftUI e RemoteViews. Flutter, quindi, non può \"disegnare\" direttamente dentro il widget.\n\nQuello che possiamo fare — ed è esattamente il compito del pacchetto [`home_widget`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fhome_widget) — è **condividere dati tra l'app Flutter e il widget nativo** e chiedere al sistema di aggiornarlo. Il flusso è sempre lo stesso:\n\n1. Flutter scrive dei valori in una memoria condivisa (`SharedPreferences` su Android, `UserDefaults` con App Group su iOS).\n2. Flutter chiede al sistema di ridisegnare il widget.\n3. Il codice nativo del widget legge quei valori e aggiorna la UI.\n\nIn questa guida costruiamo un widget \"ultima notizia\" completo: dati condivisi, deep link al tap, refresh in background e rendering di un widget Flutter come immagine.\n\n## Installazione\n\n```yaml\ndependencies:\n  home_widget: ^0.7.0\n```\n\nSul lato Dart l'API è minimale, ma **la maggior parte del lavoro è nella configurazione nativa**: senza quella, nulla funziona. Vediamola piattaforma per piattaforma.\n\n## Configurazione Android\n\n### 1. Il layout del widget\n\nCrea `android\u002Fapp\u002Fsrc\u002Fmain\u002Fres\u002Flayout\u002Fnews_widget.xml` con un layout basato su `RemoteViews` (attenzione: sono supportate solo alcune view, niente `ConstraintLayout` complessi né widget custom).\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003CLinearLayout xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n    android:id=\"@+id\u002Fwidget_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"16dp\"\n    android:background=\"#FFFFFF\">\n\n    \u003CTextView\n        android:id=\"@+id\u002Fwidget_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textSize=\"16sp\"\n        android:textStyle=\"bold\"\n        android:maxLines=\"3\"\n        android:text=\"Nessuna notizia\" \u002F>\n\n    \u003CTextView\n        android:id=\"@+id\u002Fwidget_updated\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textSize=\"12sp\"\n        android:alpha=\"0.6\" \u002F>\n\u003C\u002FLinearLayout>\n```\n\n### 2. I metadati del widget\n\nIn `android\u002Fapp\u002Fsrc\u002Fmain\u002Fres\u002Fxml\u002Fnews_widget_info.xml`:\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003Cappwidget-provider xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n    android:initialLayout=\"@layout\u002Fnews_widget\"\n    android:minWidth=\"180dp\"\n    android:minHeight=\"110dp\"\n    android:resizeMode=\"horizontal|vertical\"\n    android:updatePeriodMillis=\"1800000\"\n    android:widgetCategory=\"home_screen\" \u002F>\n```\n\n> `updatePeriodMillis` ha un minimo effettivo di 30 minuti imposto dal sistema: non usarlo per dati che cambiano spesso, usa un aggiornamento esplicito dall'app.\n\n### 3. Il provider Kotlin\n\nCrea `NewsWidgetProvider.kt` nello stesso package di `MainActivity`. Estendendo `HomeWidgetProvider` ricevi direttamente le `SharedPreferences` scritte da Flutter.\n\n```kotlin\npackage it.example.news\n\nimport android.appwidget.AppWidgetManager\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.net.Uri\nimport android.widget.RemoteViews\nimport es.antonborri.home_widget.HomeWidgetLaunchIntent\nimport es.antonborri.home_widget.HomeWidgetProvider\n\nclass NewsWidgetProvider : HomeWidgetProvider() {\n    override fun onUpdate(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetIds: IntArray,\n        widgetData: SharedPreferences\n    ) {\n        appWidgetIds.forEach { widgetId ->\n            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {\n                setTextViewText(\n                    R.id.widget_title,\n                    widgetData.getString(\"headline\", null) ?: \"Nessuna notizia\"\n                )\n                setTextViewText(\n                    R.id.widget_updated,\n                    widgetData.getString(\"updated_at\", \"\") ?: \"\"\n                )\n\n                \u002F\u002F Apre l'app su un deep link quando si tocca il widget\n                val intent = HomeWidgetLaunchIntent.getActivity(\n                    context,\n                    MainActivity::class.java,\n                    Uri.parse(\"newsapp:\u002F\u002Farticle?id=\" + widgetData.getString(\"article_id\", \"\"))\n                )\n                setOnClickPendingIntent(R.id.widget_container, intent)\n            }\n            appWidgetManager.updateAppWidget(widgetId, views)\n        }\n    }\n}\n```\n\n### 4. Registrazione nel manifest\n\nDentro il tag `\u003Capplication>` di `AndroidManifest.xml`:\n\n```xml\n\u003Creceiver\n    android:name=\".NewsWidgetProvider\"\n    android:exported=\"true\">\n    \u003Cintent-filter>\n        \u003Caction android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" \u002F>\n    \u003C\u002Fintent-filter>\n    \u003Cmeta-data\n        android:name=\"android.appwidget.provider\"\n        android:resource=\"@xml\u002Fnews_widget_info\" \u002F>\n\u003C\u002Freceiver>\n```\n\n## Configurazione iOS\n\nSu iOS servono due passaggi che spesso vengono dimenticati e che sono la causa n.1 di widget \"vuoti\".\n\n**1. App Group.** In Xcode, seleziona il target `Runner` → *Signing & Capabilities* → *+ Capability* → *App Groups* e crea un gruppo, ad esempio `group.it.example.news`. Ripeti la stessa operazione per il target del widget: **entrambi devono appartenere allo stesso gruppo**, altrimenti i dati non sono visibili.\n\n**2. Widget Extension.** Sempre in Xcode: *File → New → Target → Widget Extension*. Disattiva \"Include Live Activity\" se non ti serve. Nel codice Swift leggi i valori tramite `UserDefaults(suiteName:)`:\n\n```swift\nimport WidgetKit\nimport SwiftUI\n\nstruct NewsEntry: TimelineEntry {\n    let date: Date\n    let headline: String\n    let updatedAt: String\n}\n\nstruct Provider: TimelineProvider {\n    func placeholder(in context: Context) -> NewsEntry {\n        NewsEntry(date: Date(), headline: \"Caricamento…\", updatedAt: \"\")\n    }\n\n    func getSnapshot(in context: Context, completion: @escaping (NewsEntry) -> Void) {\n        completion(readEntry())\n    }\n\n    func getTimeline(in context: Context, completion: @escaping (Timeline\u003CNewsEntry>) -> Void) {\n        completion(Timeline(entries: [readEntry()], policy: .atEnd))\n    }\n\n    private func readEntry() -> NewsEntry {\n        let defaults = UserDefaults(suiteName: \"group.it.example.news\")\n        return NewsEntry(\n            date: Date(),\n            headline: defaults?.string(forKey: \"headline\") ?? \"Nessuna notizia\",\n            updatedAt: defaults?.string(forKey: \"updated_at\") ?? \"\"\n        )\n    }\n}\n\nstruct NewsWidgetEntryView: View {\n    var entry: NewsEntry\n\n    var body: some View {\n        VStack(alignment: .leading, spacing: 6) {\n            Text(entry.headline).font(.headline).lineLimit(3)\n            Text(entry.updatedAt).font(.caption).foregroundColor(.secondary)\n        }\n        .widgetURL(URL(string: \"newsapp:\u002F\u002Farticle\"))\n    }\n}\n\n@main\nstruct NewsWidget: Widget {\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: \"NewsWidget\", provider: Provider()) { entry in\n            NewsWidgetEntryView(entry: entry)\n        }\n        .configurationDisplayName(\"Ultima notizia\")\n        .supportedFamilies([.systemSmall, .systemMedium])\n    }\n}\n```\n\nIl valore `kind` (`NewsWidget`) è il nome che passerai a `updateWidget` da Dart.\n\n## Il codice Dart\n\nOra la parte facile. Prima di tutto, all'avvio dell'app, imposta l'App Group (viene ignorato su Android):\n\n```dart\nimport 'package:home_widget\u002Fhome_widget.dart';\n\nconst appGroupId = 'group.it.example.news';\nconst androidWidget = 'NewsWidgetProvider';\nconst iOSWidget = 'NewsWidget';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await HomeWidget.setAppGroupId(appGroupId);\n  runApp(const MyApp());\n}\n```\n\nScrivere i dati e forzare l'aggiornamento:\n\n```dart\nclass HomeWidgetService {\n  static Future\u003Cvoid> updateNews(Article article) async {\n    await Future.wait([\n      HomeWidget.saveWidgetData\u003CString>('headline', article.title),\n      HomeWidget.saveWidgetData\u003CString>('article_id', article.id),\n      HomeWidget.saveWidgetData\u003CString>(\n        'updated_at',\n        'Aggiornato alle ${DateFormat.Hm().format(DateTime.now())}',\n      ),\n    ]);\n\n    await HomeWidget.updateWidget(\n      name: androidWidget,\n      androidName: androidWidget,\n      iOSName: iOSWidget,\n      qualifiedAndroidName: 'it.example.news.$androidWidget',\n    );\n  }\n}\n```\n\nI tipi supportati da `saveWidgetData` sono quelli primitivi (`String`, `int`, `double`, `bool`). Per strutture complesse serializza in JSON e fai il parsing lato nativo, oppure — meglio — appiattisci i dati in chiavi separate: il codice nativo di un widget deve restare il più stupido possibile.\n\n## Reagire al tap: dal widget all'app\n\nCi sono due casi da gestire: l'app è già in memoria oppure viene lanciata dal widget.\n\n```dart\nclass _MyAppState extends State\u003CMyApp> {\n  @override\n  void initState() {\n    super.initState();\n    \u002F\u002F App lanciata da zero toccando il widget\n    HomeWidget.initiallyLaunchedFromHomeWidget().then(_handleUri);\n    \u002F\u002F App già in background\n    HomeWidget.widgetClicked.listen(_handleUri);\n  }\n\n  void _handleUri(Uri? uri) {\n    if (uri == null) return;\n    final id = uri.queryParameters['id'];\n    if (id != null) {\n      router.go('\u002Farticle\u002F$id');\n    }\n  }\n}\n```\n\nRicorda di dichiarare lo schema `newsapp:\u002F\u002F` nel manifest Android (intent-filter con `BROWSABLE`) e negli `URL Types` di Xcode, esattamente come per un normale deep link.\n\n## Aggiornare il widget in background\n\nL'app non è quasi mai in foreground quando il widget andrebbe aggiornato. Due strategie complementari:\n\n**1. Refresh periodico con WorkManager \u002F BGTaskScheduler.** Pianifica un task che scarica i nuovi dati e chiama `HomeWidgetService.updateNews`. Su Android il minimo è 15 minuti; su iOS il sistema decide quando eseguire, quindi considera l'aggiornamento \"best effort\".\n\n**2. Push silenziose.** Una notifica data-only via FCM che, nell'handler in background, aggiorna i dati condivisi. È l'approccio più reattivo per contenuti che cambiano di rado ma devono essere freschi.\n\nIn entrambi i casi il codice che gira in un isolate separato deve essere annotato con `@pragma('vm:entry-point')`:\n\n```dart\n@pragma('vm:entry-point')\nFuture\u003Cvoid> backgroundRefresh() async {\n  await HomeWidget.setAppGroupId(appGroupId);\n  final article = await NewsApi().fetchLatest();\n  await HomeWidgetService.updateNews(article);\n}\n```\n\n## Widget interattivi (Android 12+ \u002F iOS 17+)\n\nDalle versioni recenti dei sistemi operativi è possibile inserire pulsanti nel widget che eseguono codice **senza aprire l'app**. `home_widget` espone questa funzionalità con `registerInteractivityCallback`:\n\n```dart\n@pragma('vm:entry-point')\nFuture\u003Cvoid> interactiveCallback(Uri? uri) async {\n  if (uri?.host == 'refresh') {\n    await HomeWidget.setAppGroupId(appGroupId);\n    final article = await NewsApi().fetchLatest();\n    await HomeWidgetService.updateNews(article);\n  }\n}\n\n\u002F\u002F in main()\nawait HomeWidget.registerInteractivityCallback(interactiveCallback);\n```\n\nLato nativo il pulsante deve puntare a un `PendingIntent` generato da `HomeWidgetBackgroundIntent.getBroadcast(...)` su Android, o usare `AppIntent` con l'helper fornito dal plugin su iOS. Tieni le operazioni brevi: il sistema concede pochi secondi.\n\n## Renderizzare un widget Flutter come immagine\n\nSe la UI nativa è troppo limitante (pensa a un grafico), puoi disegnare un widget Flutter e salvarlo come PNG nella cartella condivisa, mostrandolo poi come immagine nel widget nativo:\n\n```dart\nawait HomeWidget.renderFlutterWidget(\n  SizedBox(\n    width: 300,\n    height: 150,\n    child: MiniChart(data: values),\n  ),\n  key: 'chart_image',\n  logicalSize: const Size(300, 150),\n  pixelRatio: 3,\n);\nawait HomeWidget.updateWidget(\u002F* ... *\u002F);\n```\n\nIl percorso del file finisce nella chiave `chart_image`: su Android lo leggi con `widgetData.getString(\"chart_image\", null)` e lo imposti con `setImageViewBitmap`, su iOS lo carichi con `UIImage(contentsOfFile:)`. Attenzione al peso: RemoteViews ha un limite pratico sulla dimensione dei bitmap trasferiti (transaction too large), quindi evita `pixelRatio` esagerati.\n\n## Errori comuni da evitare\n\n- **App Group mancante su un target iOS**: il widget mostra sempre i valori di default. È il 90% dei problemi.\n- **`qualifiedAndroidName` sbagliato**: se il package del provider non coincide con quello dichiarato, l'aggiornamento silenziosamente non fa nulla. Passa sempre il nome completamente qualificato.\n- **Aspettarsi aggiornamenti al secondo**: entrambi i sistemi limitano severamente la frequenza. Progetta il widget per mostrare dati che restano validi per decine di minuti.\n- **Dimenticare `@pragma('vm:entry-point')`**: funziona in debug e si rompe in release, dove il tree shaking rimuove le funzioni non referenziate.\n- **Testare solo su Android**: il ciclo di vita di WidgetKit è molto diverso; verifica sempre su dispositivo iOS reale.\n\n## Conclusione\n\n`home_widget` non trasforma Flutter in un motore di rendering per la home screen: fa qualcosa di più pragmatico, cioè crea un ponte affidabile tra la logica Dart e due tecnologie native molto diverse. Il pattern da tenere a mente è semplice — **scrivi i dati, aggiorna il widget, apri l'app con un deep link** — e da lì la complessità resta tutta nel codice nativo, che puoi mantenere volutamente minimale. Il ritorno in termini di visibilità e retention dell'app, però, è tra i più alti per lo sforzo richiesto.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F562cc9eb-5f4f-420e-93a3-2f21bbb023c1.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1622799622659-25906a2cf2d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODgwNjI1MDB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Ramal Wickramasinghe","https:\u002F\u002Funsplash.com\u002F@ramalmedia","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-cell-phone-sitting-on-top-of-a-desk-next-to-a-keyboard-UMlmcLtwEsY",null,"published","2026-08-30T04:01:40+00:00","Widget home screen in Flutter con home_widget","Guida pratica a home_widget: crea widget per la home screen di Android e iOS da Flutter, con dati condivisi, deep link, refresh in background e interattività.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789205510289]