[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-graphql-in-flutter-con-graphql-flutter-query-mutation-e-cache-normalizzata":3,"comments-article-graphql-in-flutter-con-graphql-flutter-query-mutation-e-cache-normalizzata":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},97,"GraphQL in Flutter con graphql_flutter: query, mutation e cache normalizzata","graphql-in-flutter-con-graphql-flutter-query-mutation-e-cache-normalizzata","Guida pratica a graphql_flutter: configurazione del client, cache normalizzata con Hive, query e mutation, optimistic UI, subscription realtime e generazione di codice type-safe con graphql_codegen.","## Perché GraphQL in un'app Flutter\n\nSempre più backend espongono un endpoint GraphQL accanto (o al posto) delle classiche API REST: Hasura, Shopify, GitHub, Strapi, WordPress con WPGraphQL, Apollo Server. Il vantaggio per un client mobile è concreto: **si chiede esattamente ciò che serve**, in una sola richiesta, evitando l'over-fetching tipico del REST e le decine di endpoint \"su misura\" da mantenere.\n\nIn Flutter il pacchetto di riferimento è `graphql_flutter`, costruito sopra il core `graphql`. Offre client HTTP, cache normalizzata persistente, widget dichiarativi, optimistic UI e subscription via WebSocket. In questa guida vediamo come integrarlo in modo pulito, senza trasformare i widget in un groviglio di stringhe GraphQL.\n\n## Setup del progetto\n\n```yaml\ndependencies:\n  graphql_flutter: ^5.2.0\n\ndev_dependencies:\n  build_runner: ^2.4.13\n  graphql_codegen: ^0.14.0\n```\n\nLa cache può essere persistita su disco tramite Hive, che va inizializzato prima di `runApp`:\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:graphql_flutter\u002Fgraphql_flutter.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await initHiveForFlutter(); \u002F\u002F crea il box per HiveStore\n  runApp(const MyApp());\n}\n```\n\n## Costruire il client\n\nUn `GraphQLClient` è composto da una catena di **Link** (middleware di trasporto) e da una **cache**. Un setup tipico prevede un `HttpLink`, un `AuthLink` per il token e — se servono le subscription — uno split verso il `WebSocketLink`.\n\n```dart\nclass GraphQLClientFactory {\n  static ValueNotifier\u003CGraphQLClient> create(TokenStorage tokens) {\n    final httpLink = HttpLink('https:\u002F\u002Fapi.example.com\u002Fgraphql');\n\n    final authLink = AuthLink(\n      getToken: () async {\n        final token = await tokens.read();\n        return token == null ? null : 'Bearer $token';\n      },\n    );\n\n    final wsLink = WebSocketLink(\n      'wss:\u002F\u002Fapi.example.com\u002Fgraphql',\n      config: const SocketClientConfig(\n        autoReconnect: true,\n        inactivityTimeout: Duration(seconds: 30),\n      ),\n    );\n\n    final link = Link.split(\n      (request) => request.isSubscription,\n      wsLink,\n      authLink.concat(httpLink),\n    );\n\n    return ValueNotifier(\n      GraphQLClient(\n        link: link,\n        cache: GraphQLCache(store: HiveStore()),\n        defaultPolicies: DefaultPolicies(\n          query: Policies(fetch: FetchPolicy.cacheAndNetwork),\n        ),\n      ),\n    );\n  }\n}\n```\n\nIl client va reso disponibile all'albero dei widget con `GraphQLProvider`:\n\n```dart\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return GraphQLProvider(\n      client: GraphQLClientFactory.create(TokenStorage()),\n      child: const CacheProvider(child: MaterialApp(home: ProductsPage())),\n    );\n  }\n}\n```\n\n## Le fetch policy: il cuore del comportamento offline\n\nCapire le policy evita il 90% dei problemi di \"dati vecchi\" o di spinner infiniti:\n\n- `cacheFirst`: se il dato è in cache lo restituisce senza toccare la rete. Ottimo per contenuti statici.\n- `cacheAndNetwork`: emette subito il dato in cache e poi aggiorna con la risposta di rete. È la scelta migliore per le liste, perché elimina il flash di loading.\n- `networkOnly`: ignora la cache in lettura ma la aggiorna. Utile per dati sensibili (saldo, ordini).\n- `cacheOnly`: solo cache, ideale per schermate offline.\n- `noCache`: né legge né scrive. Da usare per dati one-shot o molto grandi.\n\n## Query dichiarative con il widget `Query`\n\n```dart\nconst productsQuery = r'''\n  query Products($first: Int!, $after: String) {\n    products(first: $first, after: $after) {\n      pageInfo { hasNextPage endCursor }\n      nodes { id __typename name price }\n    }\n  }\n''';\n\nQuery(\n  options: QueryOptions(\n    document: gql(productsQuery),\n    variables: const {'first': 20},\n  ),\n  builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {\n    if (result.hasException) {\n      return ErrorView(message: result.exception.toString(), onRetry: refetch);\n    }\n    if (result.isLoading && result.data == null) {\n      return const Center(child: CircularProgressIndicator());\n    }\n\n    final nodes = (result.data!['products']['nodes'] as List).cast\u003CMap\u003CString, dynamic>>();\n    return ListView.builder(\n      itemCount: nodes.length,\n      itemBuilder: (_, i) => ListTile(\n        title: Text(nodes[i]['name'] as String),\n        subtitle: Text('${nodes[i]['price']} €'),\n      ),\n    );\n  },\n)\n```\n\nNota il `result.isLoading && result.data == null`: con `cacheAndNetwork` il risultato può essere contemporaneamente *loading* e già popolato dalla cache. Distinguere i due casi è ciò che rende l'app fluida.\n\n### Paginazione con `fetchMore`\n\n```dart\nfetchMore!(\n  FetchMoreOptions(\n    variables: {'after': pageInfo['endCursor']},\n    updateQuery: (previous, fetchMoreResult) {\n      final oldNodes = previous!['products']['nodes'] as List;\n      final newNodes = fetchMoreResult!['products']['nodes'] as List;\n      return {\n        'products': {\n          ...fetchMoreResult['products'],\n          'nodes': [...oldNodes, ...newNodes],\n        },\n      };\n    },\n  ),\n);\n```\n\n## Mutation e optimistic UI\n\nLe mutation possono aggiornare l'interfaccia **prima** della risposta del server grazie all'`optimisticResult`, che viene scritto temporaneamente in cache e sostituito (o rimosso in caso di errore) quando arriva la risposta reale.\n\n```dart\nconst toggleFavorite = r'''\n  mutation ToggleFavorite($id: ID!, $value: Boolean!) {\n    setFavorite(productId: $id, value: $value) {\n      id __typename isFavorite\n    }\n  }\n''';\n\nMutation(\n  options: MutationOptions(\n    document: gql(toggleFavorite),\n    onError: (error) => showSnackBar('Operazione non riuscita'),\n    update: (cache, result) {\n      if (result?.data == null) return;\n      cache.writeFragment(\n        Fragment(document: gql(r'''\n          fragment Fav on Product { id __typename isFavorite }\n        ''')).asRequest(idFields: {\n          '__typename': 'Product',\n          'id': result!.data!['setFavorite']['id'],\n        }),\n        data: result.data!['setFavorite'],\n      );\n    },\n  ),\n  builder: (RunMutation runMutation, QueryResult? result) {\n    return IconButton(\n      icon: Icon(product.isFavorite ? Icons.favorite : Icons.favorite_border),\n      onPressed: () => runMutation(\n        {'id': product.id, 'value': !product.isFavorite},\n        optimisticResult: {\n          'setFavorite': {\n            '__typename': 'Product',\n            'id': product.id,\n            'isFavorite': !product.isFavorite,\n          },\n        },\n      ),\n    );\n  },\n)\n```\n\n## Come funziona la cache normalizzata\n\n`GraphQLCache` non memorizza le risposte \"per query\": **normalizza** gli oggetti in una mappa piatta usando come chiave la combinazione `__typename` + `id` (o `_id`). Il risultato è che, se aggiorni un `Product` da una mutation o da un dettaglio, tutte le liste che lo contengono si aggiornano automaticamente.\n\nDue conseguenze pratiche da ricordare:\n\n1. **Includi sempre `id` e `__typename`** nelle query, altrimenti l'oggetto viene memorizzato \"inline\" nella query padre e perdi la propagazione automatica.\n2. Se il tuo schema usa una chiave diversa, configurala con le `typePolicies`:\n\n```dart\nGraphQLCache(\n  store: HiveStore(),\n  typePolicies: {\n    'Product': TypePolicy(keyFields: {'sku': true}),\n  },\n)\n```\n\nPer i dati parziali (utile con `cacheAndNetwork` su schermate di dettaglio) puoi abilitare `partialDataPolicy: PartialDataCachePolicy.acceptForOptimisticData`.\n\n## Subscription realtime\n\nCon il `WebSocketLink` già configurato, una subscription è un widget:\n\n```dart\nSubscription(\n  options: SubscriptionOptions(\n    document: gql(r'''\n      subscription OnOrderUpdated($id: ID!) {\n        orderUpdated(orderId: $id) { id __typename status updatedAt }\n      }\n    '''),\n    variables: {'id': orderId},\n  ),\n  builder: (result) {\n    if (result.isLoading) return const LinearProgressIndicator();\n    final status = result.data?['orderUpdated']['status'] as String?;\n    return Text('Stato ordine: ${status ?? '—'}');\n  },\n)\n```\n\nAttenzione al protocollo: molti server moderni usano `graphql-transport-ws`, mentre altri (Hasura in configurazioni legacy, Apollo Server 2) usano `subscriptions-transport-ws`. Il parametro `protocols` di `SocketClientConfig` permette di scegliere quello corretto.\n\n## Type-safety con graphql_codegen\n\nScrivere query come stringhe e leggere i risultati con `result.data!['products']['nodes']` è fragile. `graphql_codegen` genera classi Dart a partire dallo schema e dai file `.graphql`.\n\n```yaml\n# build.yaml\ntargets:\n  $default:\n    builders:\n      graphql_codegen:\n        options:\n          scalars:\n            DateTime:\n              type: DateTime\n              fromJsonFunctionName: dateTimeFromJson\n          clients:\n            - graphql_flutter\n```\n\nMetti le operazioni in `lib\u002Fgraphql\u002Fproducts.graphql` e lo schema in `lib\u002Fgraphql\u002Fschema.graphql`, poi esegui:\n\n```bash\ndart run build_runner build --delete-conflicting-outputs\n```\n\nOttieni widget e helper tipizzati, ad esempio:\n\n```dart\nQuery$Products$Widget(\n  options: Options$Query$Products(\n    variables: Variables$Query$Products(first: 20),\n  ),\n  builder: (result, {refetch, fetchMore}) {\n    final products = result.parsedData?.products.nodes ?? [];\n    return ProductList(products: products); \u002F\u002F oggetti Dart, non Map\n  },\n)\n```\n\n## Usare GraphQL fuori dai widget\n\nSe segui un'architettura a livelli, i widget non dovrebbero conoscere GraphQL. Incapsula il client in un repository e usa le API imperative:\n\n```dart\nclass ProductRepository {\n  ProductRepository(this._client);\n  final GraphQLClient _client;\n\n  Future\u003CList\u003CProduct>> fetchProducts({int first = 20}) async {\n    final result = await _client.query(\n      QueryOptions(\n        document: gql(productsQuery),\n        variables: {'first': first},\n        fetchPolicy: FetchPolicy.cacheAndNetwork,\n      ),\n    );\n\n    if (result.hasException) {\n      throw ProductException.from(result.exception!);\n    }\n    final nodes = result.data!['products']['nodes'] as List;\n    return nodes.map((e) => Product.fromJson(e as Map\u003CString, dynamic>)).toList();\n  }\n}\n```\n\nIn questo modo puoi combinare GraphQL con Riverpod, BLoC o qualunque altra soluzione di gestione dello stato, e sostituire il trasporto senza toccare la UI.\n\n## Gestione degli errori: due famiglie distinte\n\n`OperationException` separa nettamente:\n\n- `linkException`: problemi di trasporto (nessuna rete, timeout, risposta non JSON, 401 a livello HTTP).\n- `graphqlErrors`: errori applicativi restituiti dal server con status 200, ciascuno con `message`, `path` e `extensions` (spesso contengono un `code` come `UNAUTHENTICATED` o `FORBIDDEN`).\n\n```dart\nString mapError(OperationException e) {\n  if (e.linkException != null) return 'Connessione non disponibile';\n  final code = e.graphqlErrors.firstOrNull?.extensions?['code'];\n  return switch (code) {\n    'UNAUTHENTICATED' => 'Sessione scaduta, effettua di nuovo il login',\n    'FORBIDDEN' => 'Permessi insufficienti',\n    _ => e.graphqlErrors.firstOrNull?.message ?? 'Errore imprevisto',\n  };\n}\n```\n\nUn risultato può contenere **sia dati sia errori** (risposta parziale): controlla sempre `result.data` prima di mostrare una schermata di errore totale.\n\n## Best practice finali\n\n- **Un file `.graphql` per feature**, mai query inline sparse nei widget.\n- Usa i **fragment** per condividere la selezione di campi tra lista e dettaglio: garantiscono che la cache abbia sempre gli stessi campi.\n- Chiama `GraphQLClient.resetStore()` (o `cache.store.reset()`) al logout: la cache persistita contiene dati dell'utente precedente.\n- Evita `pollInterval` aggressivi su mobile: consumano batteria. Preferisci le subscription o un refresh legato al lifecycle dell'app.\n- Attiva la **persisted query** (APQ) se il server la supporta: riduce il payload inviando l'hash della query invece del testo.\n- Non mettere il token direttamente nell'`HttpLink`: l'`AuthLink` con `getToken` asincrono permette di leggere il token aggiornato a ogni richiesta.\n\nGraphQL non è sempre la scelta giusta — per API semplici REST resta più immediato — ma quando lo schema è ricco e le schermate aggregano molte entità, la combinazione di query mirate e cache normalizzata riduce drasticamente sia il traffico sia il codice di sincronizzazione che saresti costretto a scrivere a mano.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F8d5ae98a-2b7f-462f-a1f8-759a212746c0.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1639140651961-41392a332bfc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODkzNTg1MDF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Zheng Yang","https:\u002F\u002Funsplash.com\u002F@zyangdev","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-close-up-of-a-metal-object-on-a-blue-surface-Mh5y8htZ3SM",null,"published","2026-09-14T04:01:42+00:00","GraphQL in Flutter con graphql_flutter: guida pratica","Come integrare GraphQL in Flutter: client e link, cache normalizzata con Hive, query, mutation con optimistic UI, subscription e codegen type-safe.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316911015]