[{"data":1,"prerenderedAt":85},["ShallowReactive",2],{"tutorial-indicatori-di-caricamento-in-flutter-circularprogressindicator-e-linearprogressindicator":3,"comments-tutorial-indicatori-di-caricamento-in-flutter-circularprogressindicator-e-linearprogressindicator":84},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},108,"Indicatori di caricamento in Flutter: CircularProgressIndicator e LinearProgressIndicator","indicatori-di-caricamento-in-flutter-circularprogressindicator-e-linearprogressindicator","Impara a mostrare gli stati di attesa nella tua app Flutter: spinner indeterminati, barre di avanzamento con percentuale, bottoni con loader e dialog bloccanti. Tutorial pratico per principianti.","Ogni app, prima o poi, deve far aspettare l'utente: un download, una chiamata di rete, un salvataggio su database. Se durante l'attesa lo schermo resta fermo, l'utente pensa che l'app sia bloccata e spesso chiude tutto.\n\nFlutter mette a disposizione due widget pronti all'uso per comunicare \"sto lavorando\":\n\n- **`CircularProgressIndicator`**: il classico cerchio che gira, ideale per attese brevi o di durata sconosciuta.\n- **`LinearProgressIndicator`**: una barra orizzontale, perfetta quando conosci la percentuale di avanzamento.\n\nIn questo tutorial partiamo da zero e costruiamo una piccola schermata dimostrativa in cui vedremo:\n\n1. la differenza tra indicatore **indeterminato** e **determinato**;\n2. come collegare un indicatore a un'operazione asincrona reale;\n3. come creare un **bottone con loader** integrato;\n4. come mostrare un **dialog di caricamento** bloccante e chiuderlo in sicurezza;\n5. le personalizzazioni grafiche e le best practice da seguire.\n\nNon servono pacchetti esterni: tutto quello che useremo fa parte del Material di Flutter.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F711c88db-534f-4724-95f9-faad1c40dbf7.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1648525496828-5a4ccd78bfc3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA1Njk4NzJ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Joel Durkee","https:\u002F\u002Funsplash.com\u002F@joeldurkee","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-ferris-wheel-spinning-at-night-with-colorful-lights-QIlWVNOaMX8","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=ZXMj5CWcVxk",{"youtube_id":17,"duration_seconds":18,"published_at":19},"ZXMj5CWcVxk",133,"2026-09-28T11:20:27+00:00","beginner",30,"3.x","published","2026-09-28T04:31:13+00:00","Indicatori di caricamento in Flutter: guida pratica","Guida per principianti a CircularProgressIndicator e LinearProgressIndicator in Flutter: spinner, barre di avanzamento, bottoni con loader e dialog.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63,70,77],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},721,"Preparare il progetto e la schermata di base","Creiamo un nuovo progetto e sostituiamo il contenuto di `lib\u002Fmain.dart` con una schermata vuota che useremo come laboratorio.\n\nDa terminale:\n\n```bash\nflutter create loading_demo\ncd loading_demo\n```\n\nIl file di partenza definisce un `MaterialApp` con un `Scaffold` e una `Column` centrata: è lo spazio in cui aggiungeremo, passo dopo passo, i vari indicatori.\n\n> Nota: usiamo `useMaterial3: true` perché gli indicatori di Material 3 hanno un aspetto leggermente diverso (spessori e arrotondamenti) rispetto a Material 2.","import 'package:flutter\u002Fmaterial.dart';\n\nvoid main() => runApp(const LoadingDemoApp());\n\nclass LoadingDemoApp extends StatelessWidget {\n  const LoadingDemoApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Loading Demo',\n      theme: ThemeData(\n        colorSchemeSeed: Colors.indigo,\n        useMaterial3: true,\n      ),\n      home: const HomePage(),\n    );\n  }\n}\n\nclass HomePage extends StatefulWidget {\n  const HomePage({super.key});\n\n  @override\n  State\u003CHomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State\u003CHomePage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Indicatori di caricamento')),\n      body: const Padding(\n        padding: EdgeInsets.all(24),\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text('Qui aggiungeremo gli indicatori'),\n          ],\n        ),\n      ),\n    );\n  }\n}","dart","L'app si avvia mostrando una AppBar con il titolo e un testo segnaposto al centro dello schermo.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},722,2,"Il CircularProgressIndicator: indeterminato e determinato","`CircularProgressIndicator` ha due modalità:\n\n- **indeterminato** (comportamento di default): non passi il parametro `value`, il cerchio gira all'infinito. Si usa quando non sai quanto durerà l'operazione.\n- **determinato**: passi `value` con un numero tra `0.0` e `1.0`, e l'arco si riempie in proporzione.\n\nAttenzione a un errore classico: se metti un indicatore dentro una `Row` o una `Column` senza vincoli, può occupare più spazio del previsto. La soluzione più semplice è racchiuderlo in un `SizedBox` con larghezza e altezza fisse.\n\nSostituisci i `children` della `Column` con il codice qui sotto.","children: [\n  \u002F\u002F 1. Indeterminato: gira all'infinito\n  const SizedBox(\n    width: 48,\n    height: 48,\n    child: CircularProgressIndicator(strokeWidth: 5),\n  ),\n  const SizedBox(height: 8),\n  const Text('Indeterminato'),\n\n  const SizedBox(height: 32),\n\n  \u002F\u002F 2. Determinato: mostra il 65% di avanzamento\n  const SizedBox(\n    width: 64,\n    height: 64,\n    child: CircularProgressIndicator(\n      value: 0.65,\n      strokeWidth: 8,\n      backgroundColor: Color(0xFFE0E0E0),\n    ),\n  ),\n  const SizedBox(height: 8),\n  const Text('Determinato (65%)'),\n],","Sullo schermo vedi due cerchi: il primo ruota continuamente, il secondo è fermo e riempito per circa due terzi.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},723,3,"Collegare un LinearProgressIndicator a un avanzamento reale","La barra lineare è la scelta giusta quando puoi calcolare una percentuale (download, upload, importazione di record).\n\nSimuliamo un'operazione che avanza a scatti: un `Timer.periodic` incrementa una variabile di stato `_progress` da `0.0` a `1.0`. Ogni `setState` ridisegna la barra.\n\nPunti importanti:\n\n- `_progress` è un `double` nello **State** del widget, non una variabile globale;\n- quando arriviamo a `1.0` fermiamo il timer con `cancel()`;\n- nel `dispose()` cancelliamo sempre il timer, altrimenti rischiamo un `setState` chiamato dopo che il widget è stato smontato.\n\nRicorda di aggiungere `import 'dart:async';` in cima al file.","import 'dart:async';\n\n\u002F\u002F ...dentro _HomePageState\n\ndouble _progress = 0.0;\nTimer? _timer;\n\nvoid _avviaDownload() {\n  _timer?.cancel();\n  setState(() => _progress = 0.0);\n\n  _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {\n    setState(() {\n      _progress += 0.1;\n      if (_progress >= 1.0) {\n        _progress = 1.0;\n        timer.cancel();\n      }\n    });\n  });\n}\n\n@override\nvoid dispose() {\n  _timer?.cancel();\n  super.dispose();\n}\n\n\u002F\u002F ...nel build, dentro la Column\nWidget _buildBarra() {\n  final percentuale = (_progress * 100).toStringAsFixed(0);\n  return Column(\n    crossAxisAlignment: CrossAxisAlignment.stretch,\n    children: [\n      ClipRRect(\n        borderRadius: BorderRadius.circular(8),\n        child: LinearProgressIndicator(\n          value: _progress,\n          minHeight: 12,\n          backgroundColor: const Color(0xFFE0E0E0),\n        ),\n      ),\n      const SizedBox(height: 8),\n      Text('Download: $percentuale%', textAlign: TextAlign.center),\n      const SizedBox(height: 16),\n      FilledButton(\n        onPressed: _avviaDownload,\n        child: const Text('Avvia download'),\n      ),\n    ],\n  );\n}","Premendo \"Avvia download\" la barra si riempie progressivamente in circa 3 secondi e il testo mostra la percentuale aggiornata fino al 100%.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},724,4,"Creare un bottone con loader integrato","Il pattern più usato nelle app reali: l'utente preme \"Salva\", il bottone si disabilita e mostra uno spinner finché l'operazione non finisce. Così si evitano i doppi tap e si comunica che qualcosa sta accadendo.\n\nLa ricetta è semplice:\n\n1. una variabile booleana `_caricamento` nello State;\n2. `onPressed: _caricamento ? null : _salva` — passando `null` il bottone si disabilita da solo;\n3. il `child` del bottone cambia tra spinner e testo.\n\nLo spinner dentro un bottone deve essere piccolo: lo chiudiamo in un `SizedBox` di 18x18 con `strokeWidth: 2` e colore coerente con il testo del bottone.\n\n**Nota fondamentale**: dopo un `await`, prima di chiamare `setState`, controlla sempre `if (!mounted) return;`. Se l'utente è uscito dalla schermata nel frattempo, eviti un errore in console.","bool _caricamento = false;\n\nFuture\u003Cvoid> _salva() async {\n  setState(() => _caricamento = true);\n  try {\n    \u002F\u002F Simula una chiamata di rete\n    await Future\u003Cvoid>.delayed(const Duration(seconds: 2));\n    if (!mounted) return;\n    ScaffoldMessenger.of(context).showSnackBar(\n      const SnackBar(content: Text('Salvataggio completato')),\n    );\n  } finally {\n    if (mounted) setState(() => _caricamento = false);\n  }\n}\n\nWidget _buildBottone() {\n  return FilledButton(\n    onPressed: _caricamento ? null : _salva,\n    child: _caricamento\n        ? const SizedBox(\n            width: 18,\n            height: 18,\n            child: CircularProgressIndicator(\n              strokeWidth: 2,\n              color: Colors.white,\n            ),\n          )\n        : const Text('Salva'),\n  );\n}","Al tap il bottone diventa grigio e mostra un piccolo spinner per 2 secondi; poi torna normale e compare una SnackBar di conferma.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},725,5,"Mostrare un dialog di caricamento bloccante","Quando l'operazione blocca davvero tutta la schermata (es. il login), puoi mostrare un dialog non chiudibile.\n\nGli ingredienti sono:\n\n- `barrierDismissible: false` per impedire la chiusura toccando fuori;\n- `PopScope` con `canPop: false` per bloccare anche il tasto \"indietro\" di Android;\n- `Navigator.of(context, rootNavigator: true).pop()` per chiuderlo al termine.\n\n**Attenzione all'errore più comune**: chiudere il dialog dentro un `finally` senza verificare che sia ancora aperto. Per questo salviamo il `Navigator` in una variabile **prima** dell'`await` e usiamo `mounted`.","Future\u003Cvoid> _loginConDialog() async {\n  showDialog\u003Cvoid>(\n    context: context,\n    barrierDismissible: false,\n    builder: (_) => const PopScope(\n      canPop: false,\n      child: AlertDialog(\n        content: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            SizedBox(\n              width: 24,\n              height: 24,\n              child: CircularProgressIndicator(strokeWidth: 3),\n            ),\n            SizedBox(width: 20),\n            Expanded(child: Text('Accesso in corso...')),\n          ],\n        ),\n      ),\n    ),\n  );\n\n  final navigator = Navigator.of(context, rootNavigator: true);\n\n  try {\n    await Future\u003Cvoid>.delayed(const Duration(seconds: 3));\n  } finally {\n    if (mounted) navigator.pop(); \u002F\u002F chiude il dialog\n  }\n}","Appare un dialog con spinner e testo \"Accesso in corso...\"; per 3 secondi non è possibile chiuderlo né con il tap fuori né con il tasto indietro, poi sparisce da solo.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},726,6,"Personalizzare aspetto e adattarsi alla piattaforma","Gli indicatori prendono di default il colore primario del tema. Puoi intervenire a due livelli:\n\n**Sul singolo widget**: `color`, `backgroundColor`, `strokeWidth`, `minHeight`, oppure `valueColor` con un `AlwaysStoppedAnimation\u003CColor>` per casi particolari.\n\n**Su tutta l'app**, tramite `ProgressIndicatorThemeData` nel `ThemeData`: così non devi ripetere gli stessi colori ovunque.\n\nSe vuoi lo spinner nativo di iOS puoi usare `CupertinoActivityIndicator`, oppure il comodo `.adaptive()`, che sceglie automaticamente lo stile giusto in base alla piattaforma.","\u002F\u002F 1. Tema globale\ntheme: ThemeData(\n  colorSchemeSeed: Colors.indigo,\n  useMaterial3: true,\n  progressIndicatorTheme: const ProgressIndicatorThemeData(\n    color: Colors.indigo,\n    linearTrackColor: Color(0xFFE8EAF6),\n    linearMinHeight: 10,\n  ),\n),\n\n\u002F\u002F 2. Spinner adattivo: Material su Android\u002Fweb, Cupertino su iOS\u002FmacOS\nconst SizedBox(\n  width: 32,\n  height: 32,\n  child: CircularProgressIndicator.adaptive(),\n),\n\n\u002F\u002F 3. Colore personalizzato solo su un widget\nconst LinearProgressIndicator(\n  value: 0.4,\n  color: Colors.orange,\n  backgroundColor: Color(0xFFFFE0B2),\n),","Tutti gli indicatori dell'app adottano i colori del tema; su iOS lo spinner adattivo mostra lo stile nativo Cupertino.",{"id":78,"position":79,"title":80,"body":81,"code_snippet":82,"code_language":39,"expected_result":83,"demo_url":41,"video_url":41},727,7,"Best practice e errori da evitare","Chiudiamo con le regole d'oro imparate sul campo.\n\n**1. Non lasciare mai un loader senza via d'uscita.** Avvolgi sempre l'operazione in `try\u002Fcatch\u002Ffinally` e ripristina lo stato anche in caso di errore, altrimenti l'utente resta bloccato davanti a uno spinner eterno.\n\n**2. Controlla `mounted` dopo ogni `await`.** È la causa numero uno dell'errore *\"setState() called after dispose()\"*.\n\n**3. Usa l'indicatore determinato quando puoi.** Se conosci la percentuale, mostrala: l'attesa percepita si riduce drasticamente.\n\n**4. Per attese sotto i 300 ms non mostrare nulla.** Uno spinner che lampeggia per un istante è più fastidioso di nessun feedback.\n\n**5. Considera lo scheletro (skeleton) al posto dello spinner** per il primo caricamento di liste: l'utente capisce subito la struttura dei contenuti.\n\n**6. Accessibilità**: avvolgi il loader in un `Semantics` con un'etichetta, così gli screen reader annunciano lo stato di attesa.\n\nQui sotto, un widget riutilizzabile che incapsula queste regole: mostralo al posto del contenuto quando `isLoading` è `true`.","class LoadingOverlay extends StatelessWidget {\n  const LoadingOverlay({\n    super.key,\n    required this.isLoading,\n    required this.child,\n    this.messaggio = 'Caricamento in corso',\n  });\n\n  final bool isLoading;\n  final Widget child;\n  final String messaggio;\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        child,\n        if (isLoading)\n          Positioned.fill(\n            child: ColoredBox(\n              color: Colors.black38,\n              child: Center(\n                child: Semantics(\n                  label: messaggio,\n                  liveRegion: true,\n                  child: const SizedBox(\n                    width: 48,\n                    height: 48,\n                    child: CircularProgressIndicator(),\n                  ),\n                ),\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n}\n\n\u002F\u002F Uso:\n\u002F\u002F body: LoadingOverlay(\n\u002F\u002F   isLoading: _caricamento,\n\u002F\u002F   child: ListView(children: [...]),\n\u002F\u002F ),","Hai un componente riutilizzabile che oscura la schermata e mostra uno spinner accessibile durante le operazioni lunghe, pronto da riusare in tutta l'app.",[],1792316928884]