[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-cloud-firestore-in-flutter-query-realtime-e-regole-di-sicurezza":3,"comments-article-cloud-firestore-in-flutter-query-realtime-e-regole-di-sicurezza":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},107,"Cloud Firestore in Flutter: query, realtime e regole di sicurezza","cloud-firestore-in-flutter-query-realtime-e-regole-di-sicurezza","Guida pratica a cloud_firestore: modelli type-safe con withConverter, query composte e indici, stream realtime, paginazione, transazioni e batch, persistenza offline e regole di sicurezza da scrivere prima di andare in produzione.","Firebase Auth e Analytics sono spesso i primi servizi che integriamo in un'app Flutter, ma il cuore della maggior parte dei progetti è il database. **Cloud Firestore** è un database documentale NoSQL, realtime e con supporto offline nativo: si sposa benissimo con il modello reattivo di Flutter, ma va usato con criterio per evitare query costose, modelli fragili e — soprattutto — dati esposti a chiunque.\n\nIn questa guida vediamo come strutturare un layer dati Firestore solido: modelli type-safe, query efficienti, streaming, paginazione, scritture atomiche e regole di sicurezza.\n\n## Setup\n\nDopo aver configurato il progetto con FlutterFire CLI (`dart pub global activate flutterfire_cli` e `flutterfire configure`), aggiungi le dipendenze:\n\n```yaml\ndependencies:\n  firebase_core: ^3.8.0\n  cloud_firestore: ^5.5.0\n```\n\nInizializzazione classica in `main`:\n\n```dart\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Firebase.initializeApp(\n    options: DefaultFirebaseOptions.currentPlatform,\n  );\n\n  FirebaseFirestore.instance.settings = const Settings(\n    persistenceEnabled: true,\n    cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,\n  );\n\n  runApp(const MyApp());\n}\n```\n\nSu mobile la persistenza offline è attiva di default; su Web va abilitata esplicitamente (e funziona in una sola tab alla volta, salvo usare la persistenza multi-tab).\n\n## Modellare i dati: il modello documentale\n\nIl primo errore tipico di chi arriva da SQL è normalizzare tutto. In Firestore si paga **per documento letto**, non per byte: conviene quindi denormalizzare i dati che servono in lista (titolo, autore, thumbnail) ed evitare di dover fare N letture per riempire una schermata.\n\nRegole pratiche:\n\n- una **collection** per ogni entità principale (`posts`, `users`, `orders`);\n- usa le **subcollection** per relazioni uno-a-molti illimitate (`posts\u002F{id}\u002Fcomments`);\n- duplica i campi che ti servono per filtrare o ordinare;\n- un documento non può superare 1 MB: niente array che crescono all'infinito.\n\n## Modelli type-safe con withConverter\n\nLavorare con `Map\u003CString, dynamic>` sparsi nei widget è la via più rapida al crash in produzione. `withConverter` permette di ottenere collection e documenti già tipizzati.\n\n```dart\nclass Post {\n  const Post({\n    required this.id,\n    required this.title,\n    required this.authorId,\n    required this.tags,\n    required this.createdAt,\n    this.likes = 0,\n  });\n\n  final String id;\n  final String title;\n  final String authorId;\n  final List\u003CString> tags;\n  final DateTime createdAt;\n  final int likes;\n\n  factory Post.fromFirestore(\n    DocumentSnapshot\u003CMap\u003CString, dynamic>> doc,\n  ) {\n    final data = doc.data()!;\n    return Post(\n      id: doc.id,\n      title: data['title'] as String,\n      authorId: data['authorId'] as String,\n      tags: List\u003CString>.from(data['tags'] as List? ?? const []),\n      createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),\n      likes: (data['likes'] as num?)?.toInt() ?? 0,\n    );\n  }\n\n  Map\u003CString, dynamic> toFirestore() => {\n        'title': title,\n        'authorId': authorId,\n        'tags': tags,\n        'createdAt': FieldValue.serverTimestamp(),\n        'likes': likes,\n      };\n}\n```\n\nNota `FieldValue.serverTimestamp()`: il timestamp viene generato dal server, non dall'orologio del dispositivo (che l'utente può manipolare). Attenzione però: nello snapshot locale ottimistico il campo è `null` finché il server non conferma — per questo nel factory c'è un fallback.\n\nOra la reference tipizzata:\n\n```dart\nfinal postsRef = FirebaseFirestore.instance\n    .collection('posts')\n    .withConverter\u003CPost>(\n      fromFirestore: (snapshot, _) => Post.fromFirestore(snapshot),\n      toFirestore: (post, _) => post.toFirestore(),\n    );\n\n\u002F\u002F Lettura già tipizzata\nfinal snapshot = await postsRef.doc('abc123').get();\nfinal Post? post = snapshot.data();\n```\n\n## Query: filtri, ordinamenti e indici\n\nFirestore supporta filtri combinati, ma con vincoli precisi. Un esempio tipico:\n\n```dart\nQuery\u003CPost> recentByTag(String tag) => postsRef\n    .where('tags', arrayContains: tag)\n    .where('published', isEqualTo: true)\n    .orderBy('createdAt', descending: true)\n    .limit(20);\n```\n\nPunti da ricordare:\n\n- una query con filtri di uguaglianza su più campi **più** un `orderBy` diverso richiede un **indice composito**. Alla prima esecuzione la console stampa un link diretto per crearlo: usalo e poi versiona gli indici in `firestore.indexes.json`;\n- puoi avere un solo campo con filtri di **range** (`\u003C`, `>`, `\u003C=`, `>=`) e l'`orderBy` deve partire da quel campo;\n- `arrayContainsAny`, `whereIn` e `whereNotIn` accettano al massimo 30 valori;\n- dalla versione recente di `cloud_firestore` esistono anche i filtri `OR` tramite `Filter`:\n\n```dart\nfinal query = postsRef.where(\n  Filter.or(\n    Filter('authorId', isEqualTo: currentUserId),\n    Filter('visibility', isEqualTo: 'public'),\n  ),\n);\n```\n\nFirestore **non** ha ricerca full-text: per quella servono servizi esterni (Algolia, Typesense, Meilisearch) alimentati da Cloud Functions.\n\n## Realtime con gli stream\n\nIl valore aggiunto di Firestore è lo streaming dei cambiamenti. Il pattern più semplice lato UI:\n\n```dart\nStreamBuilder\u003CQuerySnapshot\u003CPost>>(\n  stream: recentByTag('flutter').snapshots(),\n  builder: (context, snapshot) {\n    if (snapshot.hasError) {\n      return const Center(child: Text('Errore di caricamento'));\n    }\n    if (!snapshot.hasData) {\n      return const Center(child: CircularProgressIndicator());\n    }\n\n    final posts = snapshot.data!.docs.map((d) => d.data()).toList();\n    return ListView.builder(\n      itemCount: posts.length,\n      itemBuilder: (context, i) => PostTile(post: posts[i]),\n    );\n  },\n)\n```\n\nDue accorgimenti importanti:\n\n1. **Non creare lo stream dentro il `build`**: ogni rebuild aprirebbe un nuovo listener. Memorizzalo in un campo dello `State`, in un provider Riverpod o in un repository.\n2. Usa `snapshots(includeMetadataChanges: true)` se vuoi distinguere i dati locali (scritture ottimistiche) da quelli confermati dal server, leggendo `snapshot.metadata.hasPendingWrites` e `isFromCache`.\n\nUn repository ben fatto espone stream di modelli, non di snapshot:\n\n```dart\nclass PostRepository {\n  PostRepository(this._db);\n  final FirebaseFirestore _db;\n\n  Stream\u003CList\u003CPost>> watchByTag(String tag) => _db\n      .collection('posts')\n      .withConverter\u003CPost>(\n        fromFirestore: (s, _) => Post.fromFirestore(s),\n        toFirestore: (p, _) => p.toFirestore(),\n      )\n      .where('tags', arrayContains: tag)\n      .orderBy('createdAt', descending: true)\n      .limit(50)\n      .snapshots()\n      .map((s) => s.docs.map((d) => d.data()).toList());\n}\n```\n\n## Paginazione con cursori\n\nDimentica `offset`: Firestore usa i cursori basati sull'ultimo documento caricato, molto più efficienti.\n\n```dart\nclass PostPager {\n  PostPager(this._query);\n  final Query\u003CPost> _query;\n  DocumentSnapshot\u003CPost>? _last;\n  bool hasMore = true;\n\n  Future\u003CList\u003CPost>> loadNext({int pageSize = 20}) async {\n    if (!hasMore) return const [];\n\n    var q = _query.limit(pageSize);\n    if (_last != null) q = q.startAfterDocument(_last!);\n\n    final snap = await q.get();\n    if (snap.docs.length \u003C pageSize) hasMore = false;\n    if (snap.docs.isNotEmpty) _last = snap.docs.last;\n\n    return snap.docs.map((d) => d.data()).toList();\n  }\n}\n```\n\nQuesto pager si integra perfettamente con `infinite_scroll_pagination` o con un semplice `ScrollController`.\n\n## Scritture: atomicità con transazioni e batch\n\nPer incrementare un contatore non leggere-e-riscrivere: usa gli operatori atomici lato server.\n\n```dart\nawait postsRef.doc(postId).update({\n  'likes': FieldValue.increment(1),\n  'likedBy': FieldValue.arrayUnion([userId]),\n});\n```\n\nQuando devi leggere e scrivere in modo consistente (es. scalare uno stock solo se disponibile), serve una **transazione**, che viene ritentata automaticamente in caso di conflitto:\n\n```dart\nFuture\u003Cvoid> buy(String productId, int qty) {\n  final db = FirebaseFirestore.instance;\n  final ref = db.collection('products').doc(productId);\n\n  return db.runTransaction((tx) async {\n    final snap = await tx.get(ref);\n    final stock = (snap.data()?['stock'] as num?)?.toInt() ?? 0;\n    if (stock \u003C qty) {\n      throw Exception('Stock insufficiente');\n    }\n    tx.update(ref, {'stock': stock - qty});\n    tx.set(db.collection('orders').doc(), {\n      'productId': productId,\n      'qty': qty,\n      'createdAt': FieldValue.serverTimestamp(),\n    });\n  });\n}\n```\n\nAttenzione: nelle transazioni **tutte le letture vanno fatte prima delle scritture** e le transazioni non funzionano offline (falliscono se il device non è connesso).\n\nPer scritture multiple indipendenti (fino a 500 operazioni) usa un `WriteBatch`, che è atomico e funziona anche offline:\n\n```dart\nfinal batch = FirebaseFirestore.instance.batch();\nfor (final id in idsDaArchiviare) {\n  batch.update(postsRef.doc(id), {'archived': true});\n}\nawait batch.commit();\n```\n\n## Offline-first quasi gratis\n\nCon la persistenza attiva, le scritture vengono messe in coda localmente e gli stream emettono subito i dati dalla cache: l'app resta usabile senza rete. Puoi anche forzare la sorgente:\n\n```dart\nfinal cached = await postsRef.doc(id).get(const GetOptions(source: Source.cache));\n```\n\nNon attendere `await` sulle scritture se vuoi UI ottimistica istantanea: la Future si completa solo quando il server conferma, mentre lo stream locale è già aggiornato. Gestisci comunque l'errore con un `catchError` per non avere eccezioni non catturate.\n\n## Regole di sicurezza: il passaggio che non si può saltare\n\nUn'app Flutter parla direttamente con Firestore: senza regole, chiunque estragga le chiavi dal bundle può leggere e cancellare tutto. Le regole vanno scritte **insieme** al modello dati.\n\n```\nrules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n\n    function isSignedIn() {\n      return request.auth != null;\n    }\n\n    match \u002Fposts\u002F{postId} {\n      allow read: if resource.data.published == true\n                  || (isSignedIn() && resource.data.authorId == request.auth.uid);\n\n      allow create: if isSignedIn()\n                    && request.resource.data.authorId == request.auth.uid\n                    && request.resource.data.title is string\n                    && request.resource.data.title.size() \u003C= 120;\n\n      allow update, delete: if isSignedIn()\n                            && resource.data.authorId == request.auth.uid;\n\n      match \u002Fcomments\u002F{commentId} {\n        allow read: if true;\n        allow create: if isSignedIn();\n      }\n    }\n  }\n}\n```\n\nRicorda che le regole **non filtrano i dati**: se una query può restituire documenti non consentiti, la query fallisce interamente. Le query dell'app devono quindi \"rispecchiare\" le regole (es. filtrare esplicitamente su `published == true`).\n\nPer sviluppare in sicurezza usa l'**emulatore**:\n\n```dart\nif (kDebugMode) {\n  FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);\n}\n```\n\ne testa le regole con `firebase emulators:exec --only firestore` e la suite `@firebase\u002Frules-unit-testing`.\n\n## Testare il layer dati\n\nPer i test di unità del repository esiste `fake_cloud_firestore`, che simula in memoria l'API:\n\n```dart\ntest('watchByTag restituisce solo i post del tag', () async {\n  final firestore = FakeFirebaseFirestore();\n  await firestore.collection('posts').add({\n    'title': 'Ciao',\n    'authorId': 'u1',\n    'tags': ['flutter'],\n    'createdAt': Timestamp.now(),\n  });\n\n  final repo = PostRepository(firestore);\n  final posts = await repo.watchByTag('flutter').first;\n\n  expect(posts, hasLength(1));\n  expect(posts.first.title, 'Ciao');\n});\n```\n\n## Controllare i costi\n\nFirestore fattura letture, scritture e cancellazioni. Alcune abitudini che fanno la differenza:\n\n- **sempre `limit()`** sulle query in lista, mai `get()` su collection intere;\n- evita listener duplicati: uno stream condiviso in un repository (o un `StreamProvider` di Riverpod con `autoDispose`) invece di uno per widget;\n- mantieni contatori aggregati (`commentsCount`) aggiornati con `FieldValue.increment` invece di contare documenti;\n- se ti serve solo il numero, usa le **aggregation query**: `await query.count().get()` costa molto meno di leggere i documenti;\n- chiudi le subscription nel `dispose()` per non tenere aperti listener invisibili.\n\n## Conclusioni\n\nFirestore permette di costruire in poche ore un backend reattivo e offline-first per un'app Flutter, ma la qualità del risultato dipende da tre scelte fatte all'inizio: un modello dati denormalizzato pensato per le query reali, un layer repository con modelli tipizzati tramite `withConverter`, e regole di sicurezza scritte e testate con l'emulatore. Con queste fondamenta, stream, paginazione e scritture atomiche diventano solo dettagli implementativi — e l'app resta veloce e prevedibile anche quando gli utenti crescono.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F35dacad3-4461-4732-90aa-d1856491ea6e.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1583695477819-357b45d15825?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAyMjI0NzV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Ferenc Almasi","https:\u002F\u002Funsplash.com\u002F@flowforfrank","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fblack-and-red-screen-display-1seONCyPWfQ",null,"published","2026-09-24T04:01:15+00:00","Cloud Firestore in Flutter: guida pratica completa","Come usare Cloud Firestore in Flutter: modelli type-safe, query e indici, stream realtime, paginazione con cursori, transazioni e regole di sicurezza.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316909249]