• トップ
  • ブログ一覧
  • 【Flutter】gRPCを使ってAPI通信を実装する【前編】
  • 【Flutter】gRPCを使ってAPI通信を実装する【前編】

    広告メディア事業部広告メディア事業部
    2022.11.08

    IT技術

    はじめに

    近々gRPCを使ってAPI通信するFlutterアプリを開発する機会があるので、そこに向けた事前準備&備忘録として対応内容を記録していきます。

    本記事ではgPRCの環境構築からUnary RPCを用いてFlutterアプリ上でHello, worldするまでの実装をまとめます。

    また、本記事では触れないのですが、後編の記事では下記4つの通信方式すべてのパターンで実装した内容をまとめていく予定です。

    • Unary RPC (単一リクエスト, 単一レスポンス)
    • Server streaming RPC (単一リクエスト, 複数レスポンス)
    • Client streaming RPC (複数リクエスト, 単一レスポンス)
    • Bidirectional streaming RPC (複数リクエスト, 複数レスポンス)

    gRPCとは?という部分の説明は省きますので、詳細を知りたい方は公式ドキュメントを御覧ください。

    開発環境

    開発環境は以下の通りです。

    Dart2.18.2
    Flutter3.3.4
    protoc3.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を参照しているProviderWidget が破棄されたときに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 を作成します。
    GreeterClientHelloRequest は先程サーバー側の準備で作成した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アプリで導入しようと考えている方の参考になれば幸いです。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background