[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-grpc-in-flutter-comunicazione-tipizzata-con-protobuf-e-streaming":3,"comments-article-grpc-in-flutter-comunicazione-tipizzata-con-protobuf-e-streaming":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},119,"gRPC in Flutter: comunicazione tipizzata con Protobuf e streaming","grpc-in-flutter-comunicazione-tipizzata-con-protobuf-e-streaming","Come usare gRPC in un'app Flutter: definizione dei contratti in Protobuf, generazione del codice Dart, chiamate unary e streaming, interceptor per l'autenticazione, gestione degli errori e supporto a Flutter Web con gRPC-Web.","## Perché gRPC in un'app Flutter\n\nQuando il backend espone API REST, in Flutter si finisce quasi sempre con Dio (o Retrofit) più la serializzazione JSON. Funziona bene, ma porta con sé alcuni costi ricorrenti: il contratto tra client e server vive nella documentazione (o nella testa di qualcuno), i modelli vanno riscritti a mano o generati da JSON, e lo streaming bidirezionale richiede WebSocket gestiti a parte.\n\n**gRPC** risolve questi problemi alla radice: il contratto è un file `.proto`, condiviso tra backend e client, da cui si genera codice tipizzato per entrambi. I messaggi viaggiano in formato binario (Protocol Buffers), più compatto del JSON, su HTTP\u002F2 con multiplexing delle richieste sulla stessa connessione. Lo streaming è parte del protocollo, non un'aggiunta.\n\ngRPC ha senso soprattutto quando:\n\n- il backend è già in gRPC (molto comune in architetture a microservizi Go, Kotlin, Rust, .NET);\n- servono flussi realtime (feed, posizioni, notifiche in-app, chat) senza inventare un protocollo sopra i WebSocket;\n- la banda conta: payload binari e compressione riducono traffico e latenza su reti mobili.\n\nNon ha senso se il backend è una API pubblica REST di terze parti, o se l'app gira solo sul web senza possibilità di mettere un proxy davanti al server (ci torniamo).\n\n## Setup del progetto\n\nAggiungi le dipendenze runtime e il generatore:\n\n```yaml\ndependencies:\n  grpc: ^4.0.1\n  protobuf: ^4.0.0\n\ndev_dependencies:\n  protoc_plugin: ^22.0.1\n```\n\nServe anche il compilatore `protoc` installato localmente (su macOS `brew install protobuf`, su Linux dal package manager, su Windows dai release di GitHub) e il plugin Dart nel PATH:\n\n```bash\ndart pub global activate protoc_plugin\nexport PATH=\"$PATH:$HOME\u002F.pub-cache\u002Fbin\"\n```\n\n## Il contratto: il file .proto\n\nIl file `.proto` è la **single source of truth**. Conviene tenerlo in una cartella `protos\u002F` versionata (o in un repository Git condiviso, incluso come submodule) in modo che backend e app partano sempre dalla stessa definizione.\n\n```proto\nsyntax = \"proto3\";\n\npackage chat.v1;\n\nservice ChatService {\n  \u002F\u002F Chiamata unary: una richiesta, una risposta\n  rpc GetRoom(GetRoomRequest) returns (Room);\n\n  \u002F\u002F Server streaming: una richiesta, N risposte\n  rpc WatchMessages(WatchMessagesRequest) returns (stream Message);\n\n  \u002F\u002F Bidirezionale: N richieste, N risposte\n  rpc Chat(stream Message) returns (stream Message);\n}\n\nmessage GetRoomRequest {\n  string room_id = 1;\n}\n\nmessage Room {\n  string id = 1;\n  string name = 2;\n  int32 members_count = 3;\n}\n\nmessage WatchMessagesRequest {\n  string room_id = 1;\n}\n\nmessage Message {\n  string id = 1;\n  string room_id = 2;\n  string author = 3;\n  string text = 4;\n  int64 sent_at_unix_ms = 5;\n}\n```\n\nDue regole d'oro del versionamento Protobuf: **i numeri di campo non si riusano mai** e i campi obsoleti si marcano `reserved`. Così un client vecchio continua a funzionare contro un server nuovo.\n\n## Generare il codice Dart\n\n```bash\nmkdir -p lib\u002Fsrc\u002Fgenerated\nprotoc \\\n  --dart_out=grpc:lib\u002Fsrc\u002Fgenerated \\\n  -Iprotos \\\n  protos\u002Fchat\u002Fv1\u002Fchat.proto\n```\n\nVengono prodotti quattro file per ogni `.proto`: i messaggi (`chat.pb.dart`), il client\u002Fservizio gRPC (`chat.pbgrpc.dart`), gli enum (`chat.pbenum.dart`) e i descrittori JSON (`chat.pbjson.dart`).\n\nConviene incapsulare il comando in uno script (`tool\u002Fprotogen.sh`) ed escludere la cartella generata dall'analisi statica:\n\n```yaml\n# analysis_options.yaml\nanalyzer:\n  exclude:\n    - lib\u002Fsrc\u002Fgenerated\u002F**\n```\n\n## Creare il canale e il client\n\nIl `ClientChannel` rappresenta la connessione HTTP\u002F2: va creato **una volta sola** e riusato, non per ogni chiamata.\n\n```dart\nimport 'package:grpc\u002Fgrpc.dart';\nimport 'generated\u002Fchat\u002Fv1\u002Fchat.pbgrpc.dart';\n\nClientChannel createChannel() {\n  return ClientChannel(\n    'api.example.com',\n    port: 443,\n    options: const ChannelOptions(\n      credentials: ChannelCredentials.secure(),\n      connectionTimeout: Duration(seconds: 10),\n      idleTimeout: Duration(minutes: 5),\n      userAgent: 'my-flutter-app\u002F1.0.0',\n      codecRegistry: CodecRegistry(codecs: [GzipCodec(), IdentityCodec()]),\n    ),\n  );\n}\n\nfinal channel = createChannel();\nfinal chatClient = ChatServiceClient(channel);\n```\n\nIn sviluppo, contro un server locale senza TLS, si usa `ChannelCredentials.insecure()`. Attenzione all'host su emulatore Android: `localhost` punta all'emulatore stesso, serve `10.0.2.2`.\n\nAlla chiusura dell'app (o quando l'utente fa logout) ricordati di liberare le risorse:\n\n```dart\nawait channel.shutdown();\n```\n\n## Chiamate unary e streaming\n\nUna chiamata unary è un semplice `Future`:\n\n```dart\nFuture\u003CRoom> loadRoom(String roomId) async {\n  final response = await chatClient.getRoom(\n    GetRoomRequest(roomId: roomId),\n    options: CallOptions(timeout: const Duration(seconds: 8)),\n  );\n  return response;\n}\n```\n\nUna chiamata server-streaming restituisce una `ResponseStream`, che è uno `Stream` a tutti gli effetti e si integra naturalmente con `StreamBuilder`, BLoC o Riverpod:\n\n```dart\nStream\u003CMessage> watchMessages(String roomId) {\n  final call = chatClient.watchMessages(\n    WatchMessagesRequest(roomId: roomId),\n  );\n  return call; \u002F\u002F cancellabile annullando la subscription\n}\n```\n\nPer lo streaming bidirezionale si passa uno `Stream` di richieste, tipicamente alimentato da uno `StreamController`:\n\n```dart\nclass ChatSession {\n  ChatSession(this._client);\n\n  final ChatServiceClient _client;\n  final _outgoing = StreamController\u003CMessage>();\n  ResponseStream\u003CMessage>? _incoming;\n\n  Stream\u003CMessage> connect() {\n    _incoming = _client.chat(_outgoing.stream);\n    return _incoming!;\n  }\n\n  void send(Message message) => _outgoing.add(message);\n\n  Future\u003Cvoid> dispose() async {\n    await _outgoing.close();\n    await _incoming?.cancel();\n  }\n}\n```\n\nRegola pratica: **ogni stream aperto va chiuso**. In un widget significa cancellare la subscription in `dispose()`; in un repository, esporre un metodo di chiusura e chiamarlo dal container di dependency injection.\n\n## Autenticazione e interceptor\n\nIl modo più diretto per inviare un token è il `CallOptions` con un *metadata provider*, che viene eseguito a ogni chiamata (quindi può rinfrescare il token scaduto):\n\n```dart\nCallOptions authOptions(TokenStore store) {\n  return CallOptions(\n    providers: [\n      (Map\u003CString, String> metadata, String uri) async {\n        final token = await store.validAccessToken();\n        metadata['authorization'] = 'Bearer $token';\n      },\n    ],\n    timeout: const Duration(seconds: 15),\n  );\n}\n```\n\nPer logica trasversale (logging, metriche, retry, gestione del 401) si usa invece un `ClientInterceptor` applicato all'intero client:\n\n```dart\nclass LoggingInterceptor extends ClientInterceptor {\n  @override\n  ResponseFuture\u003CR> interceptUnary\u003CQ, R>(\n    ClientMethod\u003CQ, R> method,\n    Q request,\n    CallOptions options,\n    ClientUnaryInvoker\u003CQ, R> invoker,\n  ) {\n    final stopwatch = Stopwatch()..start();\n    final response = invoker(method, request, options);\n    response.then(\n      (_) => debugPrint('${method.path} ok in ${stopwatch.elapsedMilliseconds}ms'),\n      onError: (Object e) => debugPrint('${method.path} failed: $e'),\n    );\n    return response;\n  }\n\n  @override\n  ResponseStream\u003CR> interceptStreaming\u003CQ, R>(\n    ClientMethod\u003CQ, R> method,\n    Stream\u003CQ> requests,\n    CallOptions options,\n    ClientStreamingInvoker\u003CQ, R> invoker,\n  ) {\n    debugPrint('stream aperto: ${method.path}');\n    return invoker(method, requests, options);\n  }\n}\n\nfinal chatClient = ChatServiceClient(\n  channel,\n  interceptors: [LoggingInterceptor()],\n);\n```\n\n## Gestire gli errori: GrpcError e status code\n\ngRPC non usa i codici HTTP ma un set di **status code** propri. Mapparli su errori di dominio evita di spargere `try\u002Fcatch` generici nella UI:\n\n```dart\nsealed class AppFailure {}\nclass NetworkFailure extends AppFailure {}\nclass UnauthorizedFailure extends AppFailure {}\nclass NotFoundFailure extends AppFailure {}\nclass ServerFailure extends AppFailure {\n  ServerFailure(this.message);\n  final String message;\n}\n\nAppFailure mapGrpcError(Object error) {\n  if (error is GrpcError) {\n    return switch (error.code) {\n      StatusCode.unauthenticated || StatusCode.permissionDenied =>\n        UnauthorizedFailure(),\n      StatusCode.notFound => NotFoundFailure(),\n      StatusCode.unavailable ||\n      StatusCode.deadlineExceeded => NetworkFailure(),\n      _ => ServerFailure(error.message ?? 'Errore sconosciuto'),\n    };\n  }\n  return ServerFailure(error.toString());\n}\n```\n\nI codici che vedrai più spesso in mobilità sono `unavailable` (server irraggiungibile o connessione caduta) e `deadlineExceeded` (timeout). Entrambi sono tipicamente **ritentabili**; `invalidArgument` e `notFound` no.\n\nUn retry minimale con backoff esponenziale, limitato alle sole chiamate idempotenti:\n\n```dart\nFuture\u003CT> withRetry\u003CT>(\n  Future\u003CT> Function() action, {\n  int maxAttempts = 3,\n}) async {\n  var attempt = 0;\n  while (true) {\n    try {\n      return await action();\n    } on GrpcError catch (e) {\n      final retryable = e.code == StatusCode.unavailable ||\n          e.code == StatusCode.deadlineExceeded;\n      attempt++;\n      if (!retryable || attempt >= maxAttempts) rethrow;\n      await Future\u003Cvoid>.delayed(Duration(milliseconds: 300 * (1 \u003C\u003C attempt)));\n    }\n  }\n}\n```\n\nPer gli stream, invece, il retry va gestito riaprendo la chiamata: una strategia efficace è incapsulare lo stream in un controller che, su errore `unavailable`, attende e riesegue la sottoscrizione passando l'ultimo cursore ricevuto (es. l'id dell'ultimo messaggio), così il server può riprendere da dove si era interrotto.\n\n## Flutter Web: serve gRPC-Web\n\nI browser non permettono il controllo diretto dei frame HTTP\u002F2 richiesto da gRPC, quindi su Flutter Web si usa **gRPC-Web**, che richiede un proxy davanti al server (Envoy, il filtro gRPC-Web di NGINX, oppure un server che lo supporti nativamente come ConnectRPC).\n\nIl codice generato è lo stesso: cambia solo il canale.\n\n```dart\nimport 'package:flutter\u002Ffoundation.dart' show kIsWeb;\nimport 'package:grpc\u002Fgrpc.dart';\nimport 'package:grpc\u002Fgrpc_connection_interface.dart';\nimport 'package:grpc\u002Fgrpc_web.dart';\n\nClientChannelBase buildChannel() {\n  if (kIsWeb) {\n    return GrpcWebClientChannel.xhr(Uri.parse('https:\u002F\u002Fgrpc-web.example.com'));\n  }\n  return ClientChannel(\n    'api.example.com',\n    port: 443,\n    options: const ChannelOptions(credentials: ChannelCredentials.secure()),\n  );\n}\n```\n\nDue limiti da conoscere: con gRPC-Web **lo streaming client e bidirezionale non è supportato** (solo unary e server streaming), e il proxy deve esporre gli header CORS corretti, incluso `grpc-status` e `grpc-message` tra gli `Access-Control-Expose-Headers`.\n\n## Testare senza backend\n\nPer i test, il package `grpc` consente di avviare un server in-process: definisci un'implementazione fittizia del servizio generato e fai puntare il client a `localhost`.\n\n```dart\nclass FakeChatService extends ChatServiceBase {\n  @override\n  Future\u003CRoom> getRoom(ServiceCall call, GetRoomRequest request) async {\n    return Room(id: request.roomId, name: 'Stanza di test', membersCount: 3);\n  }\n\n  @override\n  Stream\u003CMessage> watchMessages(\n    ServiceCall call,\n    WatchMessagesRequest request,\n  ) async* {\n    yield Message(id: '1', roomId: request.roomId, text: 'ciao');\n  }\n\n  @override\n  Stream\u003CMessage> chat(ServiceCall call, Stream\u003CMessage> request) =>\n      request; \u002F\u002F echo\n}\n\n\u002F\u002F nel test\nfinal server = Server.create(services: [FakeChatService()]);\nawait server.serve(port: 0);\nfinal channel = ClientChannel(\n  'localhost',\n  port: server.port!,\n  options: const ChannelOptions(credentials: ChannelCredentials.insecure()),\n);\n```\n\nÈ il modo più realistico di testare il layer di rete: verifichi serializzazione, interceptor e gestione degli errori senza mock fragili.\n\n## Best practice operative\n\n- **Isola il codice generato** dietro un repository: la UI non dovrebbe mai vedere i tipi `.pb.dart`. Converti i messaggi Protobuf in modelli di dominio (magari con Freezed) nel layer dati.\n- **Un canale per app**, gestito dal container di DI, con `shutdown()` sul teardown.\n- **Sempre un timeout** nelle `CallOptions`: senza deadline, una chiamata può restare appesa finché la connessione non cade.\n- **Attiva il keepalive** se l'app mantiene stream aperti a lungo: le reti mobili chiudono le connessioni inattive e senza ping l'app se ne accorge solo al primo messaggio perso.\n- **Riconnetti al ritorno in foreground**: combina `AppLifecycleListener` e lo stato di connettività per riaprire gli stream quando l'app torna attiva.\n- **Versiona il package** nel `.proto` (`chat.v1`) fin dall'inizio: cambiare contratto in modo incompatibile su un'app già pubblicata negli store è molto più costoso che su un backend.\n\n## Conclusione\n\ngRPC porta in Flutter un contratto condiviso e verificabile a compile time, payload compatti e streaming di prima classe. Il costo d'ingresso è la toolchain di generazione e, sul web, la necessità di un proxy gRPC-Web. In cambio si eliminano intere classi di bug — campi JSON rinominati, tipi sbagliati, documentazione disallineata — che in un'app distribuita sugli store si pagano a caro prezzo.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F188f0c3f-4ad9-4af6-b8f7-5d984f93e955.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1631864032173-c2f015fe3459?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTEyNTkzMDR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Morthy Jameson","https:\u002F\u002Funsplash.com\u002F@theothermorthy","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-laptop-computer-sitting-on-top-of-a-wooden-desk-0abr4JPOfZg",null,"published","2026-10-06T04:01:44+00:00","gRPC in Flutter: guida pratica a Protobuf e streaming","Come integrare gRPC in Flutter: file .proto, generazione del codice Dart, chiamate unary e streaming, interceptor, errori, test e gRPC-Web per Flutter Web.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316907148]