
【Flutter】gRPCを使ってAPI通信を実装する【前編】
2022.11.08
はじめに
近々gRPCを使ってAPI通信するFlutterアプリを開発する機会があるので、そこに向けた事前準備&備忘録として対応内容を記録していきます。
本記事ではgPRCの環境構築からUnary RPCを用いてFlutterアプリ上でHello, worldするまでの実装をまとめます。
また、本記事では触れないのですが、後編の記事では下記4つの通信方式すべてのパターンで実装した内容をまとめていく予定です。
- Unary RPC (単一リクエスト, 単一レスポンス)
- Server streaming RPC (単一リクエスト, 複数レスポンス)
- Client streaming RPC (複数リクエスト, 単一レスポンス)
- Bidirectional streaming RPC (複数リクエスト, 複数レスポンス)
gRPCとは?という部分の説明は省きますので、詳細を知りたい方は公式ドキュメントを御覧ください。
開発環境
開発環境は以下の通りです。
Dart | 2.18.2 |
Flutter | 3.3.4 |
protoc | 3.21.7 |
実装
まずはgRPCを使った実装の流れを把握するために、Hello, worldを出力する簡単なアプリを作成していきます。
環境構築
実装の前にDartでgRPCを利用するための環境構築を行います。
Dart
version 2.12以上のDartをインストールします。
今回はFlutter SDKのDartを参照するので省略します。
protoc
Protocol bufferのコンパイラであるprotocをインストールします。バージョンは3以上が必要です。
筆者はmacOSを使っているので、Homebrewをつかってインストールします。
1 2 | $ brew install protobuf $ protoc --version # バージョンが3.x.x以上であることを確認 |
※ バイナリをインストールして設定することも可能です。
(参考:Install pre-compiled binaries (any OS) )
Dart plugin
本記事ではDartでProtocol compilerのDartプラグインをインストールします。
1 2 | $ dart pub global activate protoc_plugin $ export PATH="$PATH:$HOME/.pub-cache/bin" |
以上で環境構築は完了です。
サーバー側
Hello, worldのサンプルリポジトリが公式リポジトリに用意されていますが、今回は学習も兼ねて同様の環境を自分で作成していきます。
まずは任意のディレクトリで下記コマンドを叩いてサーバー側のプロジェクトを作成します。
1 | $ dart create server |
続いて、プロジェクトのルートで protos ディレクトリを作成し、 helloworld.proto ファイルを protos ディレクトリ配下に作成します。
作成した helloworld.proto にHello, worldを返すサービスおよびリクエストとレスポンスを定義します。
ユーザー名を受け取って、メッセージを返却する形です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | syntax = "proto3"; package helloworld; // Hello, worldを返却するサービスを定義 service Greeter { // Hello, worldを送信 rpc SayHello (HelloRequest) returns (HelloReply) {} } // ユーザー名を含むリクエスト message HelloRequest { string name = 1; } // Hello, worldを含むレスポンス message HelloReply { string message = 1; } |
次にgRPCのDart用コードを出力していくのですが、アプリ側と生成されたコードを共有したいので、自動生成されたファイルを参照できるパッケージを作成します。
server と同じディレクトリで下記コマンドを叩いて grpc_gen パッケージを作成します。
1 | $ dart create grpc_gen |
grpc_gen/pubspec.yaml に必要なパッケージを追加し、 lib 配下に src ディレクトリを作成しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | name: grpc_gen description: A sample command-line application. version: 1.0.0 # homepage: https://www.example.com environment: sdk: '>=2.18.2 <3.0.0' dependencies: grpc: ^3.0.2 protobuf: ^2.1.0 dev_dependencies: lints: ^2.0.0 test: ^1.16.0 |
準備ができたら、gRPCのDart用コードを出力していきます。
1 2 | $ cd server $ protoc --dart_out=grpc:../grpc_gen/lib/src -Iprotos protos/helloworld.proto |
出力すると下記のような形になります。
利用する側がパッケージ内の個々のファイルを意識しなくても使えるように、 grpc_gen.dart で各ファイルをexportしておきます。
1 2 3 4 5 6 | library grpc_gen; export 'src/helloworld.pb.dart'; export 'src/helloworld.pbgrpc.dart'; export 'src/helloworld.pbgrpc.dart'; export 'src/helloworld.pbjson.dart'; |
続いて、 server ディレクトリに戻って、 server/pubspec.yaml に必要なパッケージを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | name: server description: A sample command-line application. publish_to: none version: 1.0.0 # homepage: https://www.example.com environment: sdk: '>=2.18.2 <3.0.0' dependencies: grpc: ^3.0.2 grpc_gen: path: ../grpc_gen dev_dependencies: lints: ^2.0.0 test: ^1.16.0 |
最後に GreeterServiceBase を継承した GreeterService クラスを lib/greeter_service.dart として作成し、 bin/server.dart 内でサーバーアプリを起動するmain処理を実装します。
1 2 3 4 5 6 7 8 9 10 | import 'package:grpc/grpc.dart'; import 'package:grpc_gen/grpc_gen.dart'; class GreeterService extends GreeterServiceBase { @override Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) async { // サーバー側で名前を追加して返却 return HelloReply()..message = 'Hello, ${request.name}!'; } } |
1 2 3 4 5 6 7 8 9 10 11 12 | import 'package:grpc/grpc.dart'; import 'package:server/greeter_service.dart'; Future<void> main(List<String> args) async { final server = Server( [GreeterService()], const <Interceptor>[], CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), ); await server.serve(port: 50051); print('Server listening on port ${server.port}...'); } |
これでサーバー側の実装および準備は完了です。
アプリ側
サーバー側の準備が完了したので、次はアプリ側の実装を行っていきます。
まずは server および grpc_gen と同じ階層でアプリ用のプロジェクトを作成します。
1 | $ flutter create app |
次に必要なパッケージを app/pubspec.yaml に追加していきます。
※状態管理に Riverpod 、ルーティングに GoRouter を利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | name: app description: A new Flutter project. publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=2.18.2 <3.0.0' dependencies: flutter: sdk: flutter flutter_riverpod: ^2.0.2 go_router: ^5.1.0 grpc: ^3.0.2 grpc_gen: path: ../grpc_gen dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 build_runner: ^2.3.0 go_router_builder: ^1.0.14 flutter: uses-material-design: true |
続いて、先程準備したサーバー側の処理を呼び出すための画面を実装します。
画面の機能としては、テキストの入力が完了したときにリクエストが送信され、レスポンスを受け取ると挨拶が表示されるというものを作っていきます。
まずはサーバーへアクセスするためのチャンネルを生成して返却する Provider を実装します。
Providerには autoDispose を付与して、本 Providerを参照している Providerや Widget が破棄されたときに channel.shutdown() が実行されるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /// サーバーへアクセスするためのチャンネル final channelProvider = Provider.autoDispose<ClientChannel>((ref) { final channel = ClientChannel( 'localhost', port: 50051, options: const ChannelOptions( // ローカルなのでTLSを無効化 credentials: ChannelCredentials.insecure(), ), ); ref.onDispose(() { // チャンネルが利用されなくなったら破棄 channel.shutdown(); }); return channel; }); |
次に、入力した文字列を保持する StateProviderを作成します。
入力完了時に state が更新される想定です。
1 2 3 4 | /// 入力文字列 final nameProvider = StateProvider.autoDispose<String>((ref) { return ''; }); |
続いて、サーバーから取得した挨拶情報を取得する FutureProvider を作成します。
GreeterClient と HelloRequest は先程サーバー側の準備で作成した grpc_gen パッケージからimportして利用します。
REST APIのようにJSONから型変換せずに安全に利用できるのが嬉しいですね!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /// サーバーから取得した挨拶 final greeterProvider = FutureProvider.autoDispose<String>((ref) async { // サーバーへアクセスするためのチャンネルを取得 final channel = ref.watch(channelProvider); // 入力された名前を取得 final name = ref.watch(nameProvider); if (name.isEmpty) { return ''; } // クライアント作成 final client = GreeterClient(channel); // サービス実行 final response = await client.sayHello( HelloRequest(name: name), ); // レスポンスの中からメッセージを取り出して返却 return response.message; }); |
最後に、表示する Widget を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class HelloWorldPage extends HookWidget { const HelloWorldPage({super.key}); @override Widget build(BuildContext context) { final textController = useTextEditingController(); return Scaffold( appBar: AppBar(title: const Text('Hello, world!')), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 挨拶 Consumer( builder: (context, ref, child) { // 取得した結果を設定 final greet = ref.watch(greeterProvider).valueOrNull ?? ''; return Text(greet); }, ), const SizedBox(height: 32), // 入力欄 Consumer( builder: (context, ref, child) { return TextFormField( controller: textController, onEditingComplete: () { // 入力完了時に入力文字列の状態を更新 ref .read(nameProvider.notifier) .update((state) => textController.text); }, ); }, ), ], ), ), ); } } |
全体のコードは下記のとおりです。
Hello, world用画面の全体のコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:grpc/grpc.dart'; import 'package:grpc_gen/grpc_gen.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; /// サーバーへアクセスするためのチャンネル final channelProvider = Provider.autoDispose<ClientChannel>((ref) { final channel = ClientChannel( 'localhost', port: 50051, options: const ChannelOptions( // ローカルなのでTLSを無効化 credentials: ChannelCredentials.insecure(), ), ); ref.onDispose(() { // チャンネルが利用されなくなったら破棄 channel.shutdown(); }); return channel; }); /// 入力文字列 final nameProvider = StateProvider.autoDispose<String>((ref) { return ''; }); /// サーバーから取得した挨拶 final greeterProvider = FutureProvider.autoDispose<String>((ref) async { // サーバーへアクセスするためのチャンネルを取得 final channel = ref.watch(channelProvider); // 入力された名前を取得 final name = ref.watch(nameProvider); if (name.isEmpty) { return ''; } // クライアント作成 final client = GreeterClient(channel); // サービス実行 final response = await client.sayHello( HelloRequest(name: name), options: CallOptions(compression: const GzipCodec()), ); // レスポンスの中からメッセージを取り出して返却 return response.message; }); class HelloWorldPage extends HookWidget { const HelloWorldPage({super.key}); @override Widget build(BuildContext context) { final textController = useTextEditingController(); return Scaffold( appBar: AppBar(title: const Text('Hello, world!')), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( children: [ // 挨拶 Consumer( builder: (context, ref, child) { // 取得した結果を設定 final greet = ref.watch(greeterProvider).valueOrNull ?? ''; return Text(greet); }, ), const SizedBox(height: 32), // 入力欄 Consumer( builder: (context, ref, child) { return TextFormField( controller: textController, onEditingComplete: () { // 入力完了時に入力文字列の状態を更新 ref .read(nameProvider.notifier) .update((state) => textController.text); }, ); }, ), ], ), ), ); } } |
動作確認
それでは動作確認です。
まずは、サーバー側アプリを実行します。
1 2 | $ cd server $ dart bin/server.dart |
サーバーの準備ができたらアプリを起動しテキスト入力してみると…
無事サーバーとの通信ができていることが確認できました!
まとめ
今回はgRPCを使ってHello, worldするFlutterアプリを作成しました。
gRPCを使って通信することで、JSON変換周りやAPIクライアント周りを実装する必要がないので安全に通信できるようになりますね。
サーバー側からアプリ側の実装まで一通りの流れは把握できましたが、Hello, worldでは少し物足りない部分もあったので、後編の記事では下記4つの通信方式を使って色々なパターンで実装してみようと考えています!
- Unary RPC (単一リクエスト, 単一レスポンス)
- Server streaming RPC (単一リクエスト, 複数レスポンス)
- Client streaming RPC (複数リクエスト, 単一レスポンス)
- Bidirectional streaming RPC (複数リクエスト, 複数レスポンス)
gRPCをFlutterアプリで導入しようと考えている方の参考になれば幸いです。
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン