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