[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-date-orari-e-fusi-orari-in-flutter-guida-pratica-a-datetime-intl-e-timezone":3,"comments-article-date-orari-e-fusi-orari-in-flutter-guida-pratica-a-datetime-intl-e-timezone":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},101,"Date, orari e fusi orari in Flutter: guida pratica a DateTime, intl e timezone","date-orari-e-fusi-orari-in-flutter-guida-pratica-a-datetime-intl-e-timezone","Le date sono una delle fonti di bug più subdole in un'app. Vediamo come usare DateTime senza sorprese, formattare con intl in base al locale, gestire i fusi orari con il pacchetto timezone e testare codice dipendente dall'ora.","Ogni app, prima o poi, deve mostrare una data: lo storico degli ordini, la scadenza di un abbonamento, l'orario di una prenotazione. È uno degli ambiti in cui si annidano più bug silenziosi: l'evento che appare con un giorno di anticipo, il promemoria che scatta un'ora prima dopo il cambio dell'ora, il test che passa in Italia e fallisce sulla CI configurata in UTC.\n\nIn questa guida vediamo come ragionare correttamente su date e orari in Dart e Flutter: cosa fa (e cosa **non** fa) `DateTime`, come formattare in base al locale con `intl`, come lavorare con i fusi orari reali usando il pacchetto `timezone` e come rendere testabile il codice che dipende dall'ora corrente.\n\n## DateTime: solo due modalità, locale o UTC\n\nLa prima cosa da capire è che `DateTime` in Dart **non contiene l'informazione di fuso orario**. Internamente è un istante (microsecondi dall'epoch) più un flag booleano: locale oppure UTC.\n\n```dart\nfinal now = DateTime.now();        \u002F\u002F ora locale del dispositivo\nfinal nowUtc = DateTime.now().toUtc();\n\nprint(now.isUtc);                  \u002F\u002F false\nprint(now.timeZoneName);           \u002F\u002F es. CEST (dipende dall'OS)\nprint(now.timeZoneOffset);         \u002F\u002F es. 2:00:00.000000\n```\n\n`timeZoneName` e `timeZoneOffset` riflettono la configurazione del sistema operativo, non un fuso arbitrario: **non puoi costruire un `DateTime` \"in Europe\u002FRome\" se il dispositivo è a New York**. Questo è il limite che, più tardi, risolveremo con il pacchetto `timezone`.\n\n### Parsing e serializzazione\n\nPer scambiare date con un backend usa sempre ISO 8601 in UTC:\n\n```dart\n\u002F\u002F Serializzazione\nfinal json = {'createdAt': DateTime.now().toUtc().toIso8601String()};\n\u002F\u002F -> 2024-06-01T08:30:00.000Z\n\n\u002F\u002F Parsing\nfinal a = DateTime.parse('2024-06-01T08:30:00Z');   \u002F\u002F isUtc == true\nfinal b = DateTime.parse('2024-06-01T10:30:00');    \u002F\u002F isUtc == false (locale!)\nfinal c = DateTime.tryParse('non-una-data');        \u002F\u002F null, nessuna eccezione\n```\n\nNota la differenza tra `a` e `b`: se il server invia una stringa **senza** suffisso `Z` o offset, Dart la interpreta come ora locale. È una delle cause più frequenti di date sfasate. Se sai che il backend restituisce sempre UTC ma dimentica il suffisso, normalizza esplicitamente:\n\n```dart\nDateTime parseUtc(String value) {\n  final parsed = DateTime.parse(value);\n  return parsed.isUtc ? parsed : DateTime.utc(\n    parsed.year, parsed.month, parsed.day,\n    parsed.hour, parsed.minute, parsed.second, parsed.millisecond,\n  );\n}\n```\n\n## Aritmetica: durate vs calendario\n\n`add` e `subtract` lavorano su `Duration`, cioè su **tempo trascorso**, non su giorni di calendario. Attraversando il cambio dell'ora legale il risultato può sorprendere:\n\n```dart\n\u002F\u002F 30 marzo 2025, giorno prima del passaggio all'ora legale in Italia\nfinal sabato = DateTime(2025, 3, 29, 9, 0);\nprint(sabato.add(const Duration(days: 1))); \u002F\u002F 2025-03-30 10:00 locale\n```\n\nSono passate esattamente 24 ore, ma l'orologio da parete segna un'ora in più. Se vuoi \"lo stesso orario, il giorno dopo\" usa il costruttore, che normalizza i valori fuori intervallo:\n\n```dart\nfinal domenica = DateTime(sabato.year, sabato.month, sabato.day + 1, 9, 0);\n\u002F\u002F 2025-03-30 09:00 locale\n\nfinal fineMese = DateTime(2025, 2, 31); \u002F\u002F -> 2025-03-03, nessun errore\n```\n\nAltre operazioni ricorrenti:\n\n```dart\nfinal diff = fine.difference(inizio);      \u002F\u002F Duration\nprint(diff.inDays);                        \u002F\u002F troncato per difetto\n\nprint(a.isBefore(b));\nprint(a.isAtSameMomentAs(b));              \u002F\u002F confronta istanti, non flag isUtc\n\n\u002F\u002F Confronto per sola data (Flutter, package:flutter\u002Fmaterial.dart)\nprint(DateUtils.isSameDay(a, b));\nfinal soloData = DateUtils.dateOnly(a);    \u002F\u002F mezzanotte locale\n```\n\nEvita `DateTime.now().millisecondsSinceEpoch` per misurare durate: per i tempi di esecuzione usa `Stopwatch`, che non è influenzato da cambi dell'orologio di sistema.\n\n## Formattazione con intl e il locale corrente\n\nMostrare `date.toString()` all'utente non è mai la scelta giusta. Il pacchetto [`intl`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fintl) offre `DateFormat`, che conosce i pattern localizzati di ogni lingua.\n\n```yaml\ndependencies:\n  intl: ^0.20.2\n```\n\n```dart\nimport 'package:intl\u002Fintl.dart';\n\nfinal d = DateTime(2025, 6, 1, 18, 5);\n\nDateFormat.yMMMMd('it').format(d);      \u002F\u002F 1 giugno 2025\nDateFormat.yMd('it').format(d);         \u002F\u002F 1\u002F6\u002F2025\nDateFormat.Hm('it').format(d);          \u002F\u002F 18:05\nDateFormat.jm('en_US').format(d);       \u002F\u002F 6:05 PM\nDateFormat('EEEE d MMMM', 'it').format(d); \u002F\u002F domenica 1 giugno\n```\n\nPreferisci sempre i costruttori \"skeleton\" (`yMMMMd`, `yMd`, `Hm`, `jm`) ai pattern scritti a mano: si adattano automaticamente alle convenzioni del locale, incluso il formato 12\u002F24 ore.\n\nIn un'app Flutter localizzata, il locale corrente si ricava dal `BuildContext`:\n\n```dart\nString formatData(BuildContext context, DateTime date) {\n  final locale = Localizations.localeOf(context).toString();\n  return DateFormat.yMMMMd(locale).format(date.toLocal());\n}\n```\n\nNota il `toLocal()`: se la data arriva dal backend in UTC, formattarla senza convertirla produce un orario sbagliato. Se lavori fuori da Flutter (test, codice Dart puro) inizializza i dati dei locale:\n\n```dart\nimport 'package:intl\u002Fdate_symbol_data_local.dart';\n\nvoid main() {\n  initializeDateFormatting('it_IT');\n  Intl.defaultLocale = 'it_IT';\n  \u002F\u002F ...\n}\n```\n\n### Date relative (\"3 ore fa\")\n\nPer i formati relativi puoi scrivere una funzione con `Intl.plural`, così resta traducibile:\n\n```dart\nString tempoRelativo(DateTime date, {DateTime? now}) {\n  final diff = (now ?? DateTime.now()).difference(date);\n  if (diff.inMinutes \u003C 1) return 'adesso';\n  if (diff.inHours \u003C 1) {\n    final m = diff.inMinutes;\n    return Intl.plural(m, one: '1 minuto fa', other: '$m minuti fa');\n  }\n  if (diff.inHours \u003C 24) {\n    final h = diff.inHours;\n    return Intl.plural(h, one: \"1 ora fa\", other: '$h ore fa');\n  }\n  if (diff.inDays \u003C 7) {\n    final g = diff.inDays;\n    return Intl.plural(g, one: 'ieri', other: '$g giorni fa');\n  }\n  return DateFormat.yMMMd(Intl.getCurrentLocale()).format(date);\n}\n```\n\n## Fusi orari reali con il pacchetto timezone\n\nQuando l'app deve ragionare su un fuso **diverso** da quello del dispositivo — un volo, un webinar con orario \"Europe\u002FRome\", un promemoria giornaliero alle 9:00 locali dell'utente — `DateTime` non basta. Serve il database IANA, fornito dal pacchetto [`timezone`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Ftimezone).\n\n```yaml\ndependencies:\n  timezone: ^0.10.0\n  flutter_timezone: ^4.1.0   # per leggere il fuso del dispositivo\n```\n\n```dart\nimport 'package:flutter_timezone\u002Fflutter_timezone.dart';\nimport 'package:timezone\u002Fdata\u002Flatest_all.dart' as tzdata;\nimport 'package:timezone\u002Ftimezone.dart' as tz;\n\nFuture\u003Cvoid> initTimeZones() async {\n  tzdata.initializeTimeZones();\n  final name = await FlutterTimezone.getLocalTimezone(); \u002F\u002F es. 'Europe\u002FRome'\n  tz.setLocalLocation(tz.getLocation(name));\n}\n```\n\nOra puoi costruire e convertire istanti in qualunque fuso, con gestione corretta dell'ora legale:\n\n```dart\nfinal roma = tz.getLocation('Europe\u002FRome');\nfinal newYork = tz.getLocation('America\u002FNew_York');\n\n\u002F\u002F Webinar: 1 giugno 2025, ore 18:00 ora di Roma\nfinal webinar = tz.TZDateTime(roma, 2025, 6, 1, 18, 0);\n\nprint(webinar);                                  \u002F\u002F 2025-06-01 18:00:00.000+0200\nprint(webinar.toUtc());                          \u002F\u002F 2025-06-01 16:00:00.000Z\nprint(tz.TZDateTime.from(webinar, newYork));     \u002F\u002F 2025-06-01 12:00:00.000-0400\n```\n\n`TZDateTime` estende `DateTime`, quindi funziona con `DateFormat`, con i confronti e con `difference`. Il pattern consigliato è: **memorizza sempre UTC, converti nel fuso di visualizzazione solo all'ultimo momento**.\n\n```dart\nString orarioNelFuso(DateTime utc, String tzName, String locale) {\n  final location = tz.getLocation(tzName);\n  final local = tz.TZDateTime.from(utc, location);\n  return '${DateFormat.yMMMMd(locale).format(local)} '\n         '${DateFormat.Hm(locale).format(local)} '\n         '(${local.timeZoneName})';\n}\n```\n\nQuesto approccio è indispensabile anche con `flutter_local_notifications`, che per lo scheduling richiede proprio un `TZDateTime`: senza inizializzare il database dei fusi, i promemoria ricorrenti sbagliano dopo il cambio dell'ora.\n\n## Selettori di data e ora\n\nFlutter offre widget Material pronti, che rispettano automaticamente il locale se hai configurato `flutter_localizations`:\n\n```dart\nfinal data = await showDatePicker(\n  context: context,\n  initialDate: DateTime.now(),\n  firstDate: DateTime(2020),\n  lastDate: DateTime(2030),\n  helpText: 'Seleziona la data',\n);\n\nif (data == null) return;\n\nfinal ora = await showTimePicker(\n  context: context,\n  initialTime: TimeOfDay.now(),\n);\n\nif (ora == null) return;\n\nfinal scelta = DateTime(data.year, data.month, data.day, ora.hour, ora.minute);\n```\n\n`TimeOfDay` rappresenta solo ora e minuti: va sempre combinato con una data per ottenere un istante significativo. Per mostrarlo usa `ora.format(context)`, che rispetta la preferenza 12\u002F24 ore del sistema.\n\n## Rendere testabile il codice che dipende dall'ora\n\nChiamare `DateTime.now()` sparso nel codice rende i test fragili. Inietta una sorgente di tempo, oppure usa [`package:clock`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fclock):\n\n```dart\nimport 'package:clock\u002Fclock.dart';\n\nbool isScaduto(DateTime scadenza) => clock.now().isAfter(scadenza);\n\n\u002F\u002F test\ntest('rileva un abbonamento scaduto', () {\n  withClock(Clock.fixed(DateTime.utc(2025, 6, 1)), () {\n    expect(isScaduto(DateTime.utc(2025, 5, 31)), isTrue);\n  });\n});\n```\n\nNei test che coinvolgono formattazione, fissa sempre il locale (`Intl.defaultLocale`) e usa date in UTC: la CI gira quasi sempre con fuso `UTC`, e un test basato su `DateTime.now()` locale è una regressione in attesa di accadere.\n\n## Checklist operativa\n\n- **Persisti e trasmetti in UTC**, con `toIso8601String()`; converti solo per la UI.\n- Non fidarti delle stringhe senza offset: normalizzale a UTC esplicitamente.\n- Usa il **costruttore** per l'aritmetica di calendario, `Duration` per il tempo trascorso.\n- Formatta con `DateFormat` e i costruttori skeleton, passando il locale corrente.\n- Se il dominio richiede fusi orari espliciti (viaggi, eventi, scheduling), passa a `timezone` e `TZDateTime`.\n- Astrai `DateTime.now()` dietro `clock` o un servizio iniettabile.\n- Per dati puramente di calendario (compleanni, scadenze fiscali) considera di salvare una stringa `yyyy-MM-dd` invece di un istante: eviti qualsiasi ambiguità di fuso.\n\nApplicando queste regole, le date smettono di essere una sorgente di bug intermittenti e diventano semplicemente un dettaglio di formattazione all'ultimo livello della tua UI.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F4af0ee2c-09cd-4540-82a0-652857268942.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1649089896572-21a4c57d54c2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk3MDQwOTN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Road Ahead","https:\u002F\u002Funsplash.com\u002F@roadahead_2223","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-black-and-white-photo-of-a-clock-9tKK7YweEFo",null,"published","2026-09-18T04:01:33+00:00","Date e fusi orari in Flutter: DateTime, intl, timezone","Guida pratica a date e orari in Flutter: DateTime e UTC, aritmetica sicura, formattazione con intl, fusi orari con timezone e test con package clock.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316910185]