[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-live-activities-e-dynamic-island-in-flutter-notifiche-sempre-aggiornate-su-ios":3,"comments-article-live-activities-e-dynamic-island-in-flutter-notifiche-sempre-aggiornate-su-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},132,"Live Activities e Dynamic Island in Flutter: notifiche sempre aggiornate su iOS","live-activities-e-dynamic-island-in-flutter-notifiche-sempre-aggiornate-su-ios","Come integrare le Live Activities di iOS in un'app Flutter: configurazione della Widget Extension, condivisione dati con gli App Group, aggiornamenti locali e via push ActivityKit, più l'equivalente Android con le notifiche di progresso.","## Cosa sono le Live Activities\n\nLe **Live Activities** sono quelle schede \"vive\" che su iOS compaiono nella lock screen e nella **Dynamic Island**: il countdown del rider che porta la pizza, il punteggio della partita, il timer della corsa, lo stato di un upload. Introdotte con iOS 16.1 tramite il framework **ActivityKit**, sono oggi uno degli strumenti più efficaci per tenere viva la presenza di un'app senza bombardare l'utente di notifiche push.\n\nIl punto dolente per chi sviluppa in Flutter è che **l'interfaccia di una Live Activity non può essere scritta in Flutter**: deve essere una view **SwiftUI** dentro una *Widget Extension*, perché viene renderizzata dal sistema anche quando l'app non è in esecuzione. Quello che possiamo fare da Dart è avviare, aggiornare e terminare l'attività, passandole i dati.\n\nIn questa guida vediamo il percorso completo: setup Xcode, codice SwiftUI minimo, integrazione Dart con il pacchetto `live_activities`, aggiornamenti via push e, in chiusura, l'equivalente concettuale su Android.\n\n## Prerequisiti e limiti da conoscere prima di iniziare\n\nPrima di scrivere una riga di codice, conviene mettere in chiaro i vincoli imposti da Apple, perché condizionano il design della funzionalità:\n\n- richiedono **iOS 16.1+** (Dynamic Island solo su iPhone 14 Pro e successivi; sui modelli senza isola la Live Activity resta in lock screen);\n- il payload dello stato (`ContentState`) deve essere **inferiore a 4 KB**;\n- una Live Activity resta attiva per un massimo di **8 ore**, dopodiché diventa *stale* e viene rimossa dalla lock screen entro altre 4 ore;\n- l'utente può disabilitarle per singola app: va sempre verificata la disponibilità prima di mostrare UI dedicate;\n- non possono essere avviate da background senza interazione (da iOS 17.2 è possibile avviarle via push, ma serve un'autorizzazione specifica);\n- la view SwiftUI non può usare animazioni arbitrarie, video o network: è un widget, quindi **dati dentro, pixel fuori**.\n\n## Passo 1: creare la Widget Extension in Xcode\n\nApri `ios\u002FRunner.xcworkspace` e aggiungi un target **Widget Extension** (`File > New > Target > Widget Extension`), spuntando *Include Live Activity*. Chiamalo per esempio `LiveActivityWidget`. Xcode genererà lo scheletro SwiftUI.\n\nPoi configura tre cose essenziali:\n\n1. **Info.plist del target Runner**: aggiungi la chiave `NSSupportsLiveActivities` impostata a `YES` (e `NSSupportsLiveActivitiesFrequentUpdates` a `YES` se prevedi molti aggiornamenti push).\n2. **App Group**: in *Signing & Capabilities* aggiungi la capability *App Groups* **sia a Runner sia alla Widget Extension**, con lo stesso identificativo (es. `group.com.azienda.app`). È il canale con cui Dart e SwiftUI si scambiano i dati.\n3. **Deployment target**: la extension deve puntare almeno a iOS 16.1.\n\n## Passo 2: definire attributi e view SwiftUI\n\nIl pacchetto `live_activities` adotta un approccio generico: invece di definire uno `ContentState` tipizzato per ogni feature, scrive i dati in un `UserDefaults` condiviso dall'App Group e la view li rilegge per chiave. Il risultato è uno scheletro riutilizzabile:\n\n```swift\nimport ActivityKit\nimport WidgetKit\nimport SwiftUI\n\nstruct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {\n  public typealias LiveDeliveryData = ContentState\n  public struct ContentState: Codable, Hashable { }\n  var id = UUID()\n}\n\nlet sharedDefault = UserDefaults(suiteName: \"group.com.azienda.app\")!\n\nextension LiveActivitiesAppAttributes {\n  func prefixedKey(_ key: String) -> String { \"\\(id)_\\(key)\" }\n}\n\nstruct DeliveryWidget: Widget {\n  var body: some WidgetConfiguration {\n    ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in\n      \u002F\u002F UI mostrata nella lock screen \u002F banner\n      let title = sharedDefault.string(forKey: context.attributes.prefixedKey(\"title\")) ?? \"\"\n      let eta = sharedDefault.integer(forKey: context.attributes.prefixedKey(\"eta\"))\n\n      VStack(alignment: .leading, spacing: 6) {\n        Text(title).font(.headline)\n        Text(\"Arrivo tra \\(eta) minuti\").font(.subheadline)\n        ProgressView(value: Double(60 - eta), total: 60)\n      }\n      .padding()\n      .activityBackgroundTint(Color.black.opacity(0.6))\n    } dynamicIsland: { context in\n      let eta = sharedDefault.integer(forKey: context.attributes.prefixedKey(\"eta\"))\n      return DynamicIsland {\n        DynamicIslandExpandedRegion(.leading) { Image(systemName: \"bicycle\") }\n        DynamicIslandExpandedRegion(.trailing) { Text(\"\\(eta) min\") }\n        DynamicIslandExpandedRegion(.bottom) { Text(\"Il tuo ordine è in consegna\") }\n      } compactLeading: {\n        Image(systemName: \"bicycle\")\n      } compactTrailing: {\n        Text(\"\\(eta)m\")\n      } minimal: {\n        Image(systemName: \"bicycle\")\n      }\n    }\n  }\n}\n```\n\nNota le tre modalità della Dynamic Island: **compact** (app singola attiva), **minimal** (due attività contemporanee) ed **expanded** (long press). Vanno progettate tutte e tre, altrimenti l'esperienza risulta incompleta.\n\n## Passo 3: pilotare l'attività da Dart\n\nAggiungi la dipendenza:\n\n```yaml\ndependencies:\n  live_activities: ^2.3.0\n```\n\nE incapsula l'SDK in un servizio, così il resto dell'app non dipende dal pacchetto:\n\n```dart\nimport 'package:live_activities\u002Flive_activities.dart';\n\nclass DeliveryActivityService {\n  DeliveryActivityService(this._plugin);\n\n  final LiveActivities _plugin;\n  String? _activityId;\n\n  Future\u003Cvoid> init() async {\n    await _plugin.init(appGroupId: 'group.com.azienda.app');\n  }\n\n  Future\u003Cbool> get isAvailable async => _plugin.areActivitiesEnabled();\n\n  Future\u003Cvoid> start({required String title, required int eta}) async {\n    if (!await isAvailable) return;\n    _activityId = await _plugin.createActivity(\n      {\n        'title': title,\n        'eta': eta,\n      },\n      removeWhenAppIsKilled: true,\n    );\n  }\n\n  Future\u003Cvoid> update(int eta) async {\n    final id = _activityId;\n    if (id == null) return;\n    await _plugin.updateActivity(id, {'eta': eta});\n  }\n\n  Future\u003Cvoid> end() async {\n    final id = _activityId;\n    if (id == null) return;\n    await _plugin.endActivity(id);\n    _activityId = null;\n  }\n}\n```\n\nPunti di attenzione:\n\n- **`createActivity` va chiamato con l'app in foreground**. Se l'utente avvia un ordine e poi mette l'app in background, la creazione fallisce silenziosamente.\n- Mantieni l'`activityId` in storage persistente (`shared_preferences`) oppure recuperalo con `getAllActivitiesIds()` al riavvio: senza di esso non puoi aggiornare né terminare un'attività creata in una sessione precedente.\n- Le chiavi usate in Dart devono corrispondere **esattamente** a quelle lette in SwiftUI.\n\n### Chiudere sempre il cerchio\n\nUn errore frequente è lasciare Live Activity orfane sulla lock screen. Al riavvio dell'app conviene fare pulizia:\n\n```dart\nFuture\u003Cvoid> cleanupStaleActivities() async {\n  final ids = await _plugin.getAllActivitiesIds();\n  for (final id in ids) {\n    \u002F\u002F es. se l'ordine è già concluso lato backend\n    await _plugin.endActivity(id);\n  }\n}\n```\n\n## Passo 4: aggiornare da remoto con ActivityKit push\n\nIl caso d'uso reale (consegna, partita, treno in ritardo) richiede aggiornamenti **quando l'app non gira**. ActivityKit assegna a ogni attività un **push token dedicato**, diverso dal token APNs dell'app. Il flusso è:\n\n1. l'app crea la Live Activity;\n2. riceve il push token dell'attività;\n3. lo invia al backend insieme all'ID dell'ordine;\n4. il backend invia notifiche APNs di tipo `liveactivity` al token specifico.\n\nIn Dart il token arriva da uno stream:\n\n```dart\n_plugin.activityUpdateStream.listen((update) {\n  update.map(\n    active: (state) => api.registerActivityToken(\n      activityId: state.activityId,\n      token: state.activityToken,\n    ),\n    ended: (state) => api.unregisterActivity(state.activityId),\n    stale: (state) => api.unregisterActivity(state.activityId),\n    unknown: (_) {},\n  );\n});\n```\n\nIl payload APNs lato server ha questa forma (header `apns-push-type: liveactivity`, `apns-topic: \u003Cbundle-id>.push-type.liveactivity`):\n\n```json\n{\n  \"aps\": {\n    \"timestamp\": 1735689600,\n    \"event\": \"update\",\n    \"content-state\": { \"eta\": 12, \"title\": \"Il rider è in zona\" },\n    \"alert\": { \"title\": \"Ordine in arrivo\", \"body\": \"12 minuti\" }\n  }\n}\n```\n\nCon `event: \"end\"` e il campo `dismissal-date` si chiude l'attività da remoto. Se servono aggiornamenti molto frequenti (più di qualche decina all'ora) va richiesto l'entitlement per gli *frequent updates*: il sistema applica comunque un budget e può scartare i push in eccesso, quindi progetta assumendo che **qualche aggiornamento andrà perso** e invia sempre lo stato completo, mai delta.\n\n## E su Android?\n\nAndroid non ha ActivityKit, ma l'equivalente funzionale è la **notifica persistente di progresso**. Con `flutter_local_notifications` si ottiene un risultato simile:\n\n```dart\nawait notifications.show(\n  1,\n  'Ordine in consegna',\n  'Arrivo tra 12 minuti',\n  const NotificationDetails(\n    android: AndroidNotificationDetails(\n      'delivery',\n      'Consegne',\n      ongoing: true,\n      onlyAlertOnce: true,\n      showProgress: true,\n      maxProgress: 60,\n      progress: 48,\n      category: AndroidNotificationCategory.progress,\n    ),\n  ),\n);\n```\n\nDa **Android 16** esiste inoltre il concetto di *Live Updates* (notifiche promosse con `ProgressStyle`, visibili anche in forma compatta nella status bar), che avvicina molto l'esperienza a quella iOS. L'approccio corretto in Flutter è quindi **un'astrazione comune** (`OrderProgressPresenter`) con due implementazioni dietro: Live Activity su iOS, notifica ongoing su Android.\n\n## Checklist finale\n\n- Verifica sempre `areActivitiesEnabled()` prima di promettere la funzionalità all'utente.\n- Mantieni il `ContentState` piccolo e serializzabile: niente immagini, solo stringhe e numeri (le immagini vanno messe negli asset della extension o scritte nell'App Group).\n- Usa `Text(timerInterval:)` di SwiftUI per i countdown: si aggiorna da solo senza consumare budget di push.\n- Testa su dispositivo fisico: il simulatore supporta le Live Activity, ma non riproduce fedelmente il comportamento dei push e della Dynamic Island.\n- Chiudi l'attività appena l'evento finisce: una scheda ferma a \"in consegna\" da tre ore è peggio di nessuna scheda.\n\nLe Live Activities non sono una scorciatoia grafica ma una feature di prodotto: funzionano bene solo per **eventi con inizio, progresso e fine ben definiti**. Quando ci sono, però, sono il modo più elegante per restare sullo schermo dell'utente senza invaderlo.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F3875bb26-8e68-4c93-b785-5d1bc1abefcc.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1612943848039-b8085fea5ac1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTIzODI0ODh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Leon Rohrberg","https:\u002F\u002Funsplash.com\u002F@leonrohrberg","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fperson-holding-black-iphone-5-eEUi9Jba-58",null,"published","2026-10-19T04:01:28+00:00","Live Activities in Flutter: guida a Dynamic Island","Come integrare le Live Activities iOS e la Dynamic Island in Flutter: Widget Extension SwiftUI, App Group, push ActivityKit e alternativa Android.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792392190057]