[{"data":1,"prerenderedAt":80},["ShallowReactive",2],{"tutorial-sincronizzazione-dati-offline-first-in-flutter-con-hive-e-code-di-operazioni":3,"comments-tutorial-sincronizzazione-dati-offline-first-in-flutter-con-hive-e-code-di-operazioni":79},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":15,"difficulty":16,"estimated_minutes":17,"flutter_version":18,"status":19,"published_at":20,"meta_title":21,"meta_description":22,"category":23,"author":27,"steps":29},66,"Sincronizzazione dati offline-first in Flutter con Hive e code di operazioni","sincronizzazione-dati-offline-first-in-flutter-con-hive-e-code-di-operazioni","Impara a costruire un'app offline-first in Flutter usando Hive come database locale e una coda di operazioni per sincronizzare i dati con il server quando la connessione torna disponibile.","Le app moderne devono funzionare anche senza connessione. In questo tutorial adotteremo l'approccio **offline-first**: l'utente lavora sempre sui dati locali (rapidissimi) e le modifiche vengono propagate al server tramite una **coda di sincronizzazione**.\n\nUseremo:\n- **Hive** come database NoSQL locale veloce e senza dipendenze native pesanti;\n- una **coda di operazioni pendenti** persistita anch'essa su Hive;\n- **connectivity_plus** per rilevare il ritorno della rete e far partire la sincronizzazione.\n\nAlla fine avrai una `TodoApp` capace di creare e modificare elementi offline e di riconciliarli con il backend appena possibile.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F5d4f67bc-5f4e-45f4-bd0c-39a575817220.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1645642469766-24e18657c6aa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODczMjczMjB8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Mick Haupt","https:\u002F\u002Funsplash.com\u002F@rocinante_11","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-green-bench-sitting-in-the-grass-_1ifqauIAyQ",null,"advanced",45,"3.x","published","2026-08-17T04:30:58+00:00","Offline-first in Flutter con Hive e coda di sync","Guida pratica per creare un'app offline-first in Flutter: database locale Hive, coda di operazioni pendenti e sincronizzazione automatica col server.",{"id":24,"name":25,"slug":26},1,"Guide","guide",{"id":24,"name":28},"Flutter Bot",[30,37,44,51,58,65,72],{"id":31,"position":24,"title":32,"body":33,"code_snippet":34,"code_language":35,"expected_result":36,"demo_url":15,"video_url":15},439,"Installare le dipendenze e inizializzare Hive","Aggiungi i pacchetti necessari al `pubspec.yaml`. `hive` e `hive_flutter` per il database locale, `connectivity_plus` per la rete, e i generatori per gli adapter.\n\n```yaml\ndependencies:\n  hive: ^2.2.3\n  hive_flutter: ^1.1.0\n  connectivity_plus: ^6.0.0\n  uuid: ^4.0.0\n\ndev_dependencies:\n  hive_generator: ^2.0.1\n  build_runner: ^2.4.0\n```\n\nInizializza Hive nel `main` prima di avviare l'app.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:hive_flutter\u002Fhive_flutter.dart';\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Hive.initFlutter();\n  \u002F\u002F Gli adapter verranno registrati nel prossimo passo\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const MaterialApp(home: Scaffold());\n  }\n}","dart","Il progetto compila e Hive è inizializzato, pronto ad aprire i box locali.",{"id":38,"position":39,"title":40,"body":41,"code_snippet":42,"code_language":35,"expected_result":43,"demo_url":15,"video_url":15},440,2,"Definire il modello Todo con supporto alla sincronizzazione","Creiamo il modello `Todo` come `HiveObject`. Aggiungiamo un campo `synced` per sapere se l'elemento è già stato inviato al server e un `updatedAt` utile per risolvere i conflitti.\n\nDopo aver scritto il file, genera l'adapter con:\n\n```bash\ndart run build_runner build --delete-conflicting-outputs\n```","import 'package:hive\u002Fhive.dart';\n\npart 'todo.g.dart';\n\n@HiveType(typeId: 0)\nclass Todo extends HiveObject {\n  @HiveField(0)\n  final String id;\n\n  @HiveField(1)\n  String title;\n\n  @HiveField(2)\n  bool done;\n\n  @HiveField(3)\n  bool synced;\n\n  @HiveField(4)\n  DateTime updatedAt;\n\n  Todo({\n    required this.id,\n    required this.title,\n    this.done = false,\n    this.synced = false,\n    required this.updatedAt,\n  });\n\n  Map\u003CString, dynamic> toJson() => {\n        'id': id,\n        'title': title,\n        'done': done,\n        'updatedAt': updatedAt.toIso8601String(),\n      };\n}","Viene generato `todo.g.dart` con `TodoAdapter`, senza errori di build_runner.",{"id":45,"position":46,"title":47,"body":48,"code_snippet":49,"code_language":35,"expected_result":50,"demo_url":15,"video_url":15},441,3,"Definire la coda delle operazioni pendenti","Ogni modifica offline diventa un'**operazione** (`create`, `update`, `delete`) salvata in un box dedicato. Quando la rete torna, svuoteremo questa coda inviando le operazioni al server.\n\nUsiamo un enum e un secondo `HiveType`. Ricordati di rigenerare gli adapter con build_runner.","import 'package:hive\u002Fhive.dart';\n\npart 'pending_op.g.dart';\n\n@HiveType(typeId: 1)\nenum OpType {\n  @HiveField(0)\n  create,\n  @HiveField(1)\n  update,\n  @HiveField(2)\n  delete,\n}\n\n@HiveType(typeId: 2)\nclass PendingOp extends HiveObject {\n  @HiveField(0)\n  final String todoId;\n\n  @HiveField(1)\n  final OpType type;\n\n  @HiveField(2)\n  final Map\u003CString, dynamic> payload;\n\n  @HiveField(3)\n  final DateTime createdAt;\n\n  PendingOp({\n    required this.todoId,\n    required this.type,\n    required this.payload,\n    required this.createdAt,\n  });\n}","Vengono generati gli adapter `OpTypeAdapter` e `PendingOpAdapter`.",{"id":52,"position":53,"title":54,"body":55,"code_snippet":56,"code_language":35,"expected_result":57,"demo_url":15,"video_url":15},442,4,"Registrare gli adapter e aprire i box","Torniamo al `main` per registrare gli adapter e aprire i due box: quello dei `Todo` e quello delle operazioni pendenti. Facciamo tutto prima del `runApp` per avere i box pronti all'avvio.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:hive_flutter\u002Fhive_flutter.dart';\nimport 'todo.dart';\nimport 'pending_op.dart';\n\nlate Box\u003CTodo> todoBox;\nlate Box\u003CPendingOp> opBox;\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Hive.initFlutter();\n\n  Hive.registerAdapter(TodoAdapter());\n  Hive.registerAdapter(OpTypeAdapter());\n  Hive.registerAdapter(PendingOpAdapter());\n\n  todoBox = await Hive.openBox\u003CTodo>('todos');\n  opBox = await Hive.openBox\u003CPendingOp>('pending_ops');\n\n  runApp(const MyApp());\n}","L'app si avvia con i box `todos` e `pending_ops` aperti e persistenti tra i riavvii.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":35,"expected_result":64,"demo_url":15,"video_url":15},443,5,"Creare il repository offline-first","Il repository è l'unico punto di accesso ai dati per la UI. Ogni scrittura aggiorna **subito** Hive (con `synced = false`) e accoda un'operazione. La UI resta reattiva e non attende mai la rete.\n\nQuesto è il cuore del pattern: **scrivo prima in locale, sincronizzo dopo**.","import 'package:uuid\u002Fuuid.dart';\nimport 'todo.dart';\nimport 'pending_op.dart';\nimport 'main.dart';\n\nclass TodoRepository {\n  final _uuid = const Uuid();\n\n  List\u003CTodo> getAll() => todoBox.values.toList()\n    ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));\n\n  Future\u003Cvoid> addTodo(String title) async {\n    final todo = Todo(\n      id: _uuid.v4(),\n      title: title,\n      updatedAt: DateTime.now(),\n    );\n    await todoBox.put(todo.id, todo);\n    await _enqueue(todo.id, OpType.create, todo.toJson());\n  }\n\n  Future\u003Cvoid> toggleDone(Todo todo) async {\n    todo.done = !todo.done;\n    todo.synced = false;\n    todo.updatedAt = DateTime.now();\n    await todo.save();\n    await _enqueue(todo.id, OpType.update, todo.toJson());\n  }\n\n  Future\u003Cvoid> _enqueue(\n      String todoId, OpType type, Map\u003CString, dynamic> payload) async {\n    await opBox.add(PendingOp(\n      todoId: todoId,\n      type: type,\n      payload: payload,\n      createdAt: DateTime.now(),\n    ));\n  }\n}","Aggiungere o modificare un todo aggiorna istantaneamente Hive e crea un record nella coda operazioni.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":35,"expected_result":71,"demo_url":15,"video_url":15},444,6,"Implementare il servizio di sincronizzazione","Il `SyncService` ascolta i cambi di connettività con **connectivity_plus**. Quando la rete torna online, scorre la coda in ordine, invia ogni operazione al server (qui simulata) e la rimuove dopo il successo, marcando il todo come `synced = true`.\n\nUsiamo `flush()` anche manualmente all'avvio, nel caso ci fossero operazioni rimaste in sospeso da una sessione precedente.","import 'dart:async';\nimport 'package:connectivity_plus\u002Fconnectivity_plus.dart';\nimport 'pending_op.dart';\nimport 'todo.dart';\nimport 'main.dart';\n\nclass SyncService {\n  StreamSubscription? _sub;\n  bool _syncing = false;\n\n  void start() {\n    _sub = Connectivity().onConnectivityChanged.listen((results) {\n      final online = !results.contains(ConnectivityResult.none);\n      if (online) flush();\n    });\n    flush(); \u002F\u002F tentativo all'avvio\n  }\n\n  Future\u003Cvoid> flush() async {\n    if (_syncing) return;\n    _syncing = true;\n    try {\n      final ops = opBox.values.toList()\n        ..sort((a, b) => a.createdAt.compareTo(b.createdAt));\n      for (final op in ops) {\n        await _sendToServer(op); \u002F\u002F chiamata reale al backend\n        final todo = todoBox.get(op.todoId);\n        if (todo != null) {\n          todo.synced = true;\n          await todo.save();\n        }\n        await op.delete();\n      }\n    } catch (e) {\n      \u002F\u002F Errore di rete: le operazioni restano in coda per il prossimo tentativo\n    } finally {\n      _syncing = false;\n    }\n  }\n\n  Future\u003Cvoid> _sendToServer(PendingOp op) async {\n    \u002F\u002F Simulazione: sostituisci con http\u002FDio verso il tuo backend\n    await Future.delayed(const Duration(milliseconds: 300));\n  }\n\n  void dispose() => _sub?.cancel();\n}","Quando la connessione torna, la coda viene svuotata e i todo passano a `synced = true`.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":35,"expected_result":78,"demo_url":15,"video_url":15},445,7,"Collegare tutto alla UI reattiva","Usiamo `ValueListenableBuilder` su `todoBox.listenable()` per ricostruire la lista automaticamente ad ogni modifica di Hive. Un'icona indica se l'elemento è sincronizzato (cloud) o in attesa (cloud_off).\n\nAvvia il `SyncService` nell'`initState` e ricordati di chiuderlo nel `dispose`.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:hive_flutter\u002Fhive_flutter.dart';\nimport 'todo.dart';\nimport 'main.dart';\nimport 'todo_repository.dart';\nimport 'sync_service.dart';\n\nclass TodoScreen extends StatefulWidget {\n  const TodoScreen({super.key});\n  @override\n  State\u003CTodoScreen> createState() => _TodoScreenState();\n}\n\nclass _TodoScreenState extends State\u003CTodoScreen> {\n  final repo = TodoRepository();\n  final sync = SyncService();\n\n  @override\n  void initState() {\n    super.initState();\n    sync.start();\n  }\n\n  @override\n  void dispose() {\n    sync.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Todo Offline-first')),\n      floatingActionButton: FloatingActionButton(\n        onPressed: () => repo.addTodo('Task ${DateTime.now().second}'),\n        child: const Icon(Icons.add),\n      ),\n      body: ValueListenableBuilder(\n        valueListenable: todoBox.listenable(),\n        builder: (context, Box\u003CTodo> box, _) {\n          final todos = repo.getAll();\n          return ListView.builder(\n            itemCount: todos.length,\n            itemBuilder: (context, i) {\n              final t = todos[i];\n              return ListTile(\n                leading: Checkbox(\n                  value: t.done,\n                  onChanged: (_) => repo.toggleDone(t),\n                ),\n                title: Text(t.title),\n                trailing: Icon(\n                  t.synced ? Icons.cloud_done : Icons.cloud_off,\n                  color: t.synced ? Colors.green : Colors.orange,\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n}","La lista si aggiorna in tempo reale; disattivando la rete i nuovi todo restano 'cloud_off' e diventano 'cloud_done' al ritorno della connessione.",[],1789205526853]