FlutterのgRPCは、AndroidとiOSだけを見ているうちは他の言語と大差ありません。詰まるのは、同じコードをflutter build webに通した瞬間です。ブラウザはHTTP/2のフレームを直接扱えないため、モバイルで通っていた接続がWebでは成立しません。この記事では2026年9月時点の一次情報をもとに、生成環境の作り方、チャネルとクレデンシャルの決め方、モバイル回線を前提にしたタイムアウト設定、そしてWeb版で必要になる切り替えまでを実装順に並べます。プロトコルの仕組みそのものはgRPCの仕組みとRESTとの違いに譲るのが本記事の役割です。
まとめ|Flutterで先に決める3点と落ちやすい1点
着手前に決めるのは3つです。protoファイルをアプリのリポジトリに置くかサーバ側と共有するか、生成コードをコミットするかCIで毎回起こすか、そしてWeb版をビルド対象に含めるかどうか。3つ目を後から足すと、チャネル生成の書き方とサーバ前段の構成まで巻き戻ります。
落ちやすいのはプラットフォーム差です。package:grpc/grpc.dartのチャネルはネイティブ側でしか動かず、Webビルドではブラウザ用の実装へ差し替える必要があります。この差は実行時ではなくビルド時に決まるため、条件付きインポートか専用クラスで吸収する形にしておきます。
先に結論を示しましょう。モバイル専用のアプリならgrpcパッケージ5.1系で素直に組める構成です。Web版も同じコードで出すなら、最初からGrpcOrGrpcWebClientChannelを前提にし、単項メソッドとサーバストリーミングだけで設計をそろえます。双方向ストリーミングを使う予定があるなら、Web版は別経路にする判断を先に済ませます。
| 決める項目 | モバイル専用 | Web版も含む |
|---|---|---|
| チャネル | ClientChannel | GrpcOrGrpcWeb系 |
| サーバ前段 | 不要 | Envoy等のプロキシが必要 |
| 使える通信方式 | 4方式すべて | 単項とサーバストリーミング |
| TLS | 既定で有効 | 既定で有効 |
生成環境の構成|protoc_pluginの入れ方とPATHの通し方
Flutterの場合、スタブ生成はflutter pub getでは起こりません。protoコンパイラ本体と、Dartコードを吐き出すプラグインを別々に用意し、コマンドを自分で叩く形になります。ここを最初に固めておくと、以降の工程は他言語と同じ流れに乗ります。
protocとprotoc_pluginは別物として個別に入れる
必要なものは2つです。1つはprotoコンパイラ本体で、pub.devの記載では3.0.0以上が前提になります。もう1つがDart用の出力プラグインprotoc_pluginで、2026年9月時点の最新は25.0.0系です。プラグインはpubの公開パッケージなので、グローバル有効化で実行ファイルとして置きます。
dart pub global activate protoc_plugin
export PATH="$PATH:$HOME/.pub-cache/bin"
protoc --version
pub-cacheのbinをPATHへ通さないと生成が失敗する
グローバル有効化の直後は、ここでつまずきます。protocはプラグインをprotoc-gen-dartという名前の実行ファイルとしてPATHから探す仕組みです。~/.pub-cache/binがPATHに入っていなければ、protoc側はプラグインが存在しないというエラーで停止する状態です。シェルの設定ファイルへ書くか、CIならジョブごとにexportします。PATHを触りたくない場合は、protocの--plugin引数で実行ファイルの位置を明示する方法もあります。
pubspecへ書く依存とチーム内でのバージョン固定の考え方
生成されたDartコードはprotobufパッケージに依存するため、これをアプリ側の依存に入れます。gRPCの呼び出し側はgrpcパッケージが担当します。2026年9月時点のgrpcは5.1.0系で、5.1.0ではprotobufの6.0.0が要求されるようになりました。プラグインの版がチーム内でずれると生成物の差分がノイズになるため、dev_dependenciesにも入れて版を固定しておくと差分が読めます。
dependencies:
grpc: ^5.1.0
protobuf: ^6.0.0
dev_dependencies:
protoc_plugin: ^25.0.0
Flutter本体の版はgRPCの動作を直接左右しませんが、パッケージ側が要求するDart SDKの下限を満たす必要があります。現行のFlutter 3.47系での移行判断とあわせて、SDK下限を先に確認しておくと依存解決で止まりません。
protoからの生成|dart_outの出力先とimport解決の決め方
生成コマンドは1行ですが、出力先の決め方で後のimportが読みやすさを変えます。Flutterのプロジェクト構成に合わせてlib配下へ置くのが扱いやすい形です。
grpcプレフィックス付きのdart_outで2種類のファイルが出る
--dart_outにgrpc:のプレフィックスを付けると、メッセージ定義のpb.dartに加えてサービスのスタブを含むpbgrpc.dartが出る仕様です。プレフィックスを忘れるとメッセージだけが生成され、クライアントクラスが見つからないという症状になります。出力先ディレクトリは事前に作成しておきます。
mkdir -p lib/src/generated
protoc --dart_out=grpc:lib/src/generated \
-Iprotos protos/greeter.proto
importするのはpbgrpc.dartでpb.dartは間接参照になる
アプリ側から読むのはpbgrpc.dartです。このファイルがメッセージ側を参照しているため、両方を明示的に読む必要はありません。-Iで指定したパスからの相対位置がそのまま出力の階層になるので、protoのディレクトリを深く切ると生成物の階層も深くなります。protoの配置とimportの見え方は連動していると考えると迷いません。
生成物をリポジトリへコミットするかCIで毎回起こすかの判断軸
判断は2軸です。protoをサーバ側リポジトリと共有しているならCI生成に寄せる形です。共有元が別チームの場合、コミット済みの生成物は更新漏れの温床になりかねません。逆にprotoがアプリ側で完結しているなら、生成物をコミットしたほうがビルド環境の再現性は高まるでしょう。CIでprotocとプラグインを毎回入れる手間と、差分レビューのノイズを見比べて決めます。サーバ側をGoで書くならGoでの生成手順とprotoの置き場をそろえておくと、両者の生成が1つのコマンド群に収まります。
チャネル設計|ClientChannelの寿命とTLSの既定値の扱い
生成が通ったら、次はチャネルです。Flutterで事故が起きやすいのはこの層で、原因はほぼ寿命の設計にあります。ClientChannelはHTTP/2の接続を内部に抱えるため、画面ごとに作ると接続が増え続けます。
チャネルはウィジェット単位で作らずアプリ全体で1本だけに寄せる
チャネルは接続プールに近い性質を持ちます。StatefulWidgetのinitStateで生成する書き方は動きはしますが、画面を行き来するたびに新しい接続が張られ、モバイル回線では無視できない遅延と電池消費になります。DIコンテナか単一のリポジトリ層に置き、スタブだけを各画面へ配るのが素直な形です。層の切り方はFlutterのアーキテクチャ設計の基本の考え方に乗せると、チャネルの置き場所も自然に決まります。
アプリ終了時や明示的なログアウト時にはshutdownを呼びます。呼ばないまま参照を捨てると、進行中のRPCが宙に浮いたままになります。
credentialsの既定はsecureで開発時だけinsecureへ落とす
ChannelOptionsのcredentialsは既定がChannelCredentials.secure()です。つまり何も書かなければTLS前提の動作です。ローカルのサーバがTLSなしの場合だけChannelCredentials.insecure()へ落とす形にし、本番のビルド構成では通らないようにフラグで縛ります。Androidは平文通信が既定で許可されないため、insecureのまま実機へ載せると接続そのものが弾かれます。
final channel = ClientChannel(
'api.example.com',
port: 443,
options: ChannelOptions(
credentials: const ChannelCredentials.secure(),
connectTimeout: Duration(seconds: 5),
idleTimeout: Duration(minutes: 3),
),
);
connectTimeoutとconnectionTimeoutは別物として指定する
名前が似ている2つを混同すると、狙った挙動になりません。connectTimeoutは接続確立を待つ上限で、pub.devの記載ではシステム側のタイムアウトが短い場合はそちらが先に効きます。connectionTimeoutは1本の接続を新規リクエストへ使い回す上限時間です。idleTimeoutは呼び出しがない状態で接続を維持する時間で、短くすれば電池には優しく、次の呼び出しでは再接続の待ちが入ります。
| 設定項目 | 効く場面 | 置き方の目安 |
|---|---|---|
| connectTimeout | 接続確立まで | 数秒で打ち切る |
| connectionTimeout | 接続の使い回し上限 | 既定のまま |
| idleTimeout | 無通信時の維持 | 数分に絞る |
| backoffStrategy | 再接続の間隔 | 既定のまま |
モバイル回線での前提|デッドラインとキープアライブと再接続の設計
サーバ間通信と違い、モバイルの回線は途切れます。地下鉄に入る、機内モードを切り替える、バックグラウンドへ回る。この3つはどれも接続を壊すため、切れた前提で設定を置きます。
CallOptionsのtimeoutでRPCごとの上限を必ず置く
CallOptionsが受け取るのはmetadata・timeout・providers・compressionです。このうちtimeoutがデッドラインに相当し、指定がなければ待ち続ける形です。画面の描画を止めて待つ呼び出しには短い上限、バックグラウンドの同期には長い上限と、用途別に分けて渡します。providersに認証トークンの取得処理を渡せば、呼び出しごとにヘッダを組み立てる処理を1か所へ寄せられます。
final stub = GreeterClient(channel);
final reply = await stub.sayHello(
HelloRequest()..name = 'issoh',
options: CallOptions(
timeout: const Duration(seconds: 8),
metadata: {'authorization': 'Bearer ...'},
),
);
ClientKeepAliveOptionsはpingIntervalの指定が要る
キープアライブはchangelog上では3.2.2で入った機能で、ClientKeepAliveOptionsとして指定します。注意点はpingIntervalが省略可能な引数で、書かなければpingが飛ばないという点です。timeoutは既定20秒で、ping応答がない場合の切断待ち時間を意味します。permitWithoutCallsは既定falseなので、進行中の呼び出しがない接続は維持されません。長寿命のストリームを持つ画面がある場合だけtrueを検討します。
options: ChannelOptions(
keepAlive: const ClientKeepAliveOptions(
pingInterval: Duration(seconds: 30),
timeout: Duration(seconds: 20),
permitWithoutCalls: false,
),
),
再接続はbackoffStrategyに任せて自前のループを持たない
切断のたびに自分でリトライを書くと、指数バックオフの実装が二重になります。チャネルはbackoffStrategyを持っており、既定の戦略で再接続の間隔を広げます。アプリ側に必要なのは、失敗したRPCを再送してよいかの判定だけです。冪等でない呼び出しを機械的に再送すると二重登録が起きるため、判定基準はgRPCのステータスコードとリトライ可否です。長寿命のストリームを扱う画面では、切断そのものを異常として扱わず再購読で復帰させる設計が要ります。この考え方はgRPCストリーミングの切断対策の整理がそのまま使えます。
Web版ビルドの制約|gRPC-Webへの切り替えと分岐の書き方
ここがFlutter固有の分かれ目です。モバイルで動いたコードをそのままWebへ出すと、通信は成立しません。ブラウザはHTTP/2のフレームをアプリコードから直接組み立てられず、gRPCの規定どおりの通信ができないからです。
Webビルドではプロキシを経由しないとgRPCサーバへ到達できない
ブラウザから呼ぶ場合は、gRPC-Webという別プロトコルを経由します。gRPC-Web本体のREADMEには「gRPC-web clients connect to gRPC services via a special proxy; by default, gRPC-web uses Envoy」と書かれており、サーバの前段にプロキシを置く構成が前提です。つまりWeb版を出す判断は、アプリ側のコードだけでなくサーバ側のインフラ構成に波及します。プロキシ側の具体的な組み方はgRPC-Webの仕組みとプロキシ構成で整理しています。
Dart側の入口はpackage:grpc/grpc_web.dartです。GrpcWebClientChannel.xhrがURIを必須引数に取り、XHRベースでプロキシのエンドポイントへ接続します。ホストとポートを渡すClientChannelとは引数の形が違うため、単純な差し替えでは書き換えが広範囲に及びます。
GrpcOrGrpcWebClientChannelで分岐を1か所へ閉じる
この差を吸収する手段が用意されています。package:grpc/grpc_or_grpcweb.dartのGrpcOrGrpcWebClientChannelは、pub.devの説明に「web以外のすべてのプラットフォームではClientChannelを、webではGrpcWebClientChannelを内部で使う」と明記されたクラスです。changelog上では3.0.1で導入されています。呼び出し側はこのクラスだけを見ればよく、プラットフォーム判定をアプリコードへ書かずに済みます。
コンストラクタは3系統あります。gRPCとgRPC-Webを同じ入口へまとめるtoSingleEndpoint、同一ホストのポートだけ分けるtoSeparatePorts、ホストごと分けるtoSeparateEndpointsです。プロキシを別サブドメインへ置く構成が多いため、実務では後ろの2つを選ぶ場面が増えます。
final channel = GrpcOrGrpcWebClientChannel.toSeparateEndpoints(
grpcHost: 'api.example.com',
grpcPort: 443,
grpcTransportSecure: true,
grpcWebHost: 'grpcweb.example.com',
grpcWebPort: 443,
grpcWebTransportSecure: true,
);
注意点として、このクラスはプロトコルの差を消してくれるわけではありません。pub.devの説明にも、gRPCとgRPC-Webは別のプロトコルであり、サーバが両方に対応している必要があると書かれています。片側だけ用意した状態でWebビルドを走らせても通りません。
クライアント側と双方向のストリーミングはWeb版では使えない
設計に効く制約はこれです。gRPC-WebのREADMEには「Client-side and Bi-directional streaming is not currently supported」とあり、サーバストリーミングもgrpcwebtextモードでのみサポートされると記載されています。Web版を出す前提なら、protoの設計時点で単項メソッドとサーバストリーミングだけに寄せておくのが安全です。モバイル専用の機能として双方向ストリーミングを持つなら、Web版ではその画面を出さない判断を先に決めます。
採用の境界|FlutterでgRPCを選ぶ条件とRESTへ戻す場面
ここまでの工程を踏まえて、採用の判断を条件付きで言い切ります。
FlutterのクライアントでgRPCを選んでよい3つの条件
1つ目は、サーバ側がすでにgRPCで動いていることです。protoが存在し、サーバ側の言語で生成が回っているなら、クライアントを追加する追加費用は小さく済みます。2つ目は、通信の型を崩したくない画面があることです。protoを唯一の定義として両側から生成するため、フィールドの追加漏れがビルド時に判明します。3つ目は、ビルド対象がモバイルに限られることです。Web版を含めない判断ができるなら、制約の大半は消えます。
RESTやgRPC-Gatewayへ寄せたほうがよい3つの場面
逆に見送る場面もはっきりしています。第一に、サーバ側にプロキシを置く体制が取れない場合です。Web版を出す要件があるのにインフラ側の担当が別で、前段の構成変更に時間がかかるなら、RESTを1本用意したほうが早く終わります。第二に、外部の第三者へAPIを開ける予定がある場合です。protoの配布と生成を相手側に求めることになるため、HTTPとJSONのほうが摩擦が小さくなります。第三に、通信量が小さく呼び出し頻度も低い場合です。この条件では変換の手間に対する見返りが小さく、既存のHTTPクライアントで足ります。
両立させたい場合の逃げ道もあります。protoを唯一の定義に保ったまま、サーバ側にRESTの入口を生やす方法です。仕組みと移行の判断はgRPC-GatewayでprotoからRESTを生やす構成にまとめています。protoを軸に置いたまま、クライアント側の事情でHTTPを選べる状態にしておくと、Flutter側の判断を後ろへずらせます。
protoの設計からモバイルとWebの両対応までを含む開発の相談先を探している場合は、Flutter・React Nativeアプリ開発で対応範囲を確認できます。サーバ側の実装体制とあわせて検討できると、判断の手戻りが減ります。
よくある質問
生成コマンドは変更のたびに手で叩く必要がありますか?
手で叩くか、スクリプトへ寄せるかのどちらかです。.NETのようにビルドへ組み込まれる仕組みはDart側にないため、protoを更新したら生成を回す運用になります。Makefileやシェルスクリプトへ1行で書けるようにしておき、CIでも同じスクリプトを呼ぶ形にすると、生成漏れによるビルド失敗を防げます。
protoc_pluginはdev_dependenciesに入れるだけで足りますか?
実行ファイルとして呼ぶ場合は、グローバル有効化かPATHの解決が別途要ります。dev_dependenciesへの記載はチーム内で版をそろえる意味を持ちますが、それだけでprotoc-gen-dartがPATH上に現れるわけではありません。CIでは有効化のコマンドをステップとして明示しておくと、環境差で止まりません。
Web版だけ通信が失敗する場合はどこを見ますか?
見る順番は3つです。まずチャネルの生成箇所がClientChannelのままになっていないか。次にサーバ前段のプロキシが立っており、gRPC-Webのリクエストを受ける設定になっているか。最後に、呼んでいるメソッドがクライアントストリーミングや双方向になっていないかを確認します。3つ目は実装が正しくても仕様上通らないため、protoの定義まで戻る判断が必要になります。
サーバストリーミングをStreamBuilderで受け取れますか?
受け取り可能です。生成されたスタブはDartのStreamを返すため、ウィジェット側でもそのまま購読可能です。注意点は購読の解除で、画面破棄時にキャンセルしなければ接続が残ります。disposeでの解除と、切断時の再購読はセットで実装します。
サーバがGoやPythonでもFlutterから呼べますか?
呼べます。protoが同じであれば、サーバ側の言語は問いません。相互接続で問題になるのはprotoの版ずれとTLSの設定です。サーバ側の生成手順はPythonでのスタブ生成やC#でのビルド時生成で個別に整理しています。どちらの言語でも、protoの置き場を共有してからクライアントを起こす順序が安全です。
関連記事
- gRPCとは?仕組み・RESTとの違い・UDP/TCPの扱いを実装例で解説
- gRPC-Webとは?ブラウザからgRPCを呼ぶ仕組みとプロキシ構成・採用判断を解説
- gRPCストリーミングとは?4方式の使い分けと切断対策を実装者向けに解説
- gRPC-Gatewayとは?protoからRESTを生やす仕組みと生成構成・移行判断を解説
- gRPC Goの実装|protoc-gen-go-grpcの生成手順とNewClient移行を解説
- gRPC Pythonの実装|スタブ生成からasync・FastAPI併存まで解説
- gRPC C#の実装|Grpc.AspNetCoreのビルド時生成とホスティング制約を解説