🎸

モバイルとの相性最強と言われるgRPCをFlutter x NestJSで実装し、Stream通信や認証、複数言語実装に使えるか試す

まとめ

  • 相性バツグンといわれる、モバイル x gRPCは思ったよりずっと簡単に実装可能
  • 複数言語間でもProtocol Buffersの恩恵により型変換を意識することなくスムーズに開発が進められる。
  • メソッド、引数の型、引数の返り値の型が自動生成されるのでとても良い
  • RESTful APIにおけるheaderを、表現力の高いMetaDataとして利用し、認証認可等にも使えそう
  • Streamをうまく使いこなせば、ユーザー体験をめっちゃ高くできそう。チャットやゲームなどの双方向通信が比較的楽に実装できるかも

どんな人向きでない記事?

  • NestJSの詳しい実装を知りたい方
  • Bidirectional streaming, Client streamの詳細実装を知りたい方

モバイル向け通信技術の本格的な選択肢、gRPCを実際に試してみたい

現在、私の働いているMinediaで開発しているサービス群は、大きなアーキテクチャの改変時期を迎えており、BFF(backend for frontend)やGraphQLの導入について技術選定を行っているところです。

gRPCはバイナリを用いて通信を圧縮し通信量を削減できたり、ストリーミング配信などを簡単に実装できるため、モバイルと相性バツグンといわれています。

一方、gRPCの主戦場はマイクロサービスです。
開発元がgoogleであることも重なり、その実装例はgoが多いように見受けられます。

gRPCは公式でDartにも対応しており、ネットにはいくつか実装例もありますが、まだまだ多いとは言えません。

gRPCのDartの実装例を調べてみると、以下のような課題を見つけました。

  • 実装例の多くは単一言語で実装されていて肝心の複数言語間での開発体験がいまいちイメージできない
  • RESTful APIで当たり前のように使われているtokenの利用方法が不明
  • gRPCの面白い部分であるStreamが実装されていない

古い情報も多いようです。

そこで今回は、実際に手を動かしてこれらの部分を実装し、開発者体験がどんなものか検証してみようと考えました。

そもそもgRPCとは

https://grpc.io/
gRPCとは、googleが開発したオープンソースのHTTP/2を利用した通信技術です。

RPC(Remote Procedure Calls)とは、クライアントからローカルオブジェクトのように別のサーバーにあるメソッドを直接呼び出すための技術です。

googleのRPCフレームワークなので、gRPCという名前がついています。

gRPCにおける開発では、Protocol Buffersという言語を用いてメソッドや引数、戻り値を定義し、自動生成によりそれらを複数の言語間で共有します。

つまり、お互いに違う言語で構成されたクライアントとサーバーにおいて、それらを意識せずに関数の呼び出しが可能になることがウリです。

対応言語は C# / .NET, C++, Dart, Go, Java, Kotlin, Node, Objective-C, PHP, Python, Rubyなど多岐に渡ります。

Flutterでモバイル、他の言語でバックエンドを構築していると、JsonなReq/Resを作ったときString/int or double, timestamp/DateTimeなどの変換で詰まったり余計な時間を食ったりっての、あるあるじゃないですか?。

そういうBadな体験が自動生成の恩恵で減ればめっちゃうれしいですよね。

さらに、バイナリ化により通信効率も良くStreamを利用して利用者体験を向上させるような実装もできるとのことで、チャレンジしてみるしかない…!!と思いました。

今回の検証内容:Flutterから直接gRPCサーバーを呼び出す!

https://engineering.mercari.com/blog/entry/20210810-mercari-shops-tech-stack/
上記のように、近年ではフロントエンドが直接通信するBFFとの間をGraphQLで実装し、裏側で走っているマイクロサービス間での通信をgRPCで行うようなことが多いようです。

今回の試みでは、gRPCを利用し、Flutter側から直接サーバーサイドのメソッドを呼び出してみます。

  • 自動生成されたコードによって、複数言語間でもよい開発体験は得られるのか。
  • RESTful APIでheaderにtokenを添付するようなことは、gRPCでも行えるのか。
  • Streamを使って、UXを向上させることができるか。

これらを検証していきます。

多言語間の開発体験をシミュレートするためにClientとServerは別言語で実装

  • FrontはFlutter(Dart)
  • ServerはNestJS(TypeScript)

実装例はGoが多いので、得意な方はServer側はGoで書くとヘルプも多くて良いと思います。Go, gRPC, Flutterだと、フルGoogleスタックアーキテクチャになります(笑)

私はGoの開発経験がないので、NestJSで実装していきます。NestJSがもつGuardの機能をうまく使いながら、トークンを検証できるところまで実装してみます。

gRPCにおける4つの通信方式

名称 リクエスト : レスポンス 備考
Unary 1:1 RESTful API とほぼおなじ
Server streaming 1: n 時間がかかる処理などに対して、複数回サーバーがレスポンスを返す
Client streaming n : 1 アップロード処理などデータを分割してサーバーに送る場合などに使われる
Bidirectional streaming n : n ゲームやチャットなど、双方向に任意のタイミングで通信する

Unaryを実装するだけでは面白くないので、本記事ではServer stremingも実装していきます。

Server streaming ではクライアントのリクエストに対して、複数回のレスポンスを返すことができます。モバイルアプリの処理本体がサーバーサイドにあることはあるあるですが、進捗状況やその他のメタデータについて処理中にサーバーからクライアントへレスポンスを返すことができたら、アプリの表現力が広がります。

今回は、そんな処理の一端を体験してみましょう。

gRPC(Protocol Buffers)を利用したおおまかな開発の流れ

  1. .protoファイルでメソッドの名前、引数とその型、戻り値を定義
  2. コンパイラ(protoc)を使って、サーバーとクライアントのコードを自動生成
  3. 自動生成されたメソッドや型を使ってプログラミングする

ここまで長々と書いてきましたが、実際の手順は非常にシンプル。

既存のツール群を利用してファイルを自動生成し、それを利用するだけです。
これはgRPCというより、Protocol Buffersのすごさですね。

早速、自動生成していきましょう!

Protocol Buffersによる自動生成

Protocol Buffers コンパイラ(protoc)のインストール

https://github.com/protocolbuffers/protobuf/releases/

protocとはProtocol Buffersなファイルを各言語のコードに変換するためのコンパイラです。自動生成できなければ意味がないので、必ずインストールしておきます。

各OS用のバイナリをGitHubのリリースページからダウンロードして、パスを通しておきます。

MacやLinuxの場合パッケージマネージャ経由でインストールできるため簡単です。

  • Macの場合
brew install protobuf
  • Linuxの場合
apt install -y protobuf-compiler

インストールが終わったら、パスが通っているか以下で確認しておきましょう。

protoc --version

.protoファイルでスキーマを作成していく!

https://protobuf.dev/
Protocol Buffersについてあまり説明してきませんでしたが、以下の例を見ればわかる通り、構造化されたデータフォーマットのことです。Prisma.jsでいう .schema や、GraphQLでいう .graphql のようなものです。

sample.proto
syntax = "proto3";

package sample;

// timeStampはimportすれば使えるようになる
import "google/protobuf/timestamp.proto";

// messageが型のようなもの
// 自前の型も入れ子にして利用可能
// 配列を使いたい場合repeatedとする
message Sample {
    int32 id = 1;
    string name = 2;
    SampleType type = 3;
    ChildSample child = 4;
    google.protobuf.Timestamp updatedAt = 5;
    repeated string tags = 6;
}

// enumも利用可能
enum SampleType {
    NORMAL = 0;
    SPECIAL = 1;
}

// 入れ子になる型
message ChildSample {
    string id = 1;
    string childName = 2;
}

// メソッド群を定義
service SampleService {
    rpc GetSample (GetSampleRequest) returns (Sample) {}
    rpc GetStreamingSample (GetSampleRequest) returns (stream StreamResult) {}
}

// リクエストにも型が必要
message GetSampleRequest {
        string id = 1;
}

message StreamResult {
        int32 value = 1;
}

GraphQLにはコードの内容をもとにスキーマを生成するコードファーストなアプローチがありますが、Protocol Buffers + gRPCの実装においてはスキーマファースト開発が強制される特徴があります。

スキーマファーストな開発体験は好みがわかれるところではありますが、スキーマが必ず先にできるため、サーバー・クライアントともに型さえ守っていればお互いの言語環境や型変換を意識せずに開発を進められることが大きなメリットとなります。

大きなチームではスキーマファイルの管理についてチーム内でよく話し合っておく必要があるでしょう。

NestJSで最小限の実装を行っていく

protocol bufferからTypeScriptの型定義ファイルを作る

protoファイルが書けたら、それらを各言語のファイルに変換する必要があります。サーバーから始めたいので、TypeScriptの型定義ファイルを先に自動生成していきます。

まずは、必要なものをインストールしていきましょう。

// NestJSプロジェクトの作成
npm i -g @nestjs/cli
nest new sample-project-name

// 必要モジュールのインストール
npm install ts-proto
npm install nestjs-proto-gen-ts
npm install @grpc/grpc-js @nestjs/microservices

ディレクトリ構造は以下のようにします。
serverディレクトリにNest.jsのプロジェクトを入れて、その階層にprotoというディレクトリを準備。その中にsample.proto

.
├── proto // sample.protoを配置
├── server
│   ├── src
│   │   ├── gen_type //ここに生成
│   │   └── guard
│   └── test

コード生成は以下のようにします。
serverディレクトリから実装するときのコマンドです。
package.jsonに入れておくと、すぐgenerateできるのでおススメ。

windows
protoc -I ../proto --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_out=./src/gen_type --ts_proto_opt=nestJs=true --ts_proto_opt=addGrpcMetadata=true --ts_proto_opt=addNestjsRestParameter=true ../proto/sample.proto

Mac, Linux
protoc -I ../proto --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/gen_type --ts_proto_opt=nestJs=true --ts_proto_opt=addGrpcMetadata=true --ts_proto_opt=addNestjsRestParameter=true ../proto/sample.proto

package.jsonに入れておくと便利
"proto:generate": "protoc -I ../proto --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_out=./src/gen_type --ts_proto_opt=nestJs=true --ts_proto_opt=addGrpcMetadata=true --ts_proto_opt=addNestjsRestParameter=true ../proto/sample.proto"

https://github.com/stephenh/ts-proto/issues/93#issuecomment-966003457

NestJSで必要最小限の実装を行う

まずは必要最小限の実装だけ。

RESTful APIと似た、gRPCの最も簡単なリクエスト:レスポンス = 1:1 の Unary gRPCを実装していきます。

NestJSの実装
npm i -g /cli
nest new sample-project-name
//main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GrpcOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<GrpcOptions>(
    AppModule, {
      transport: Transport.GRPC,
      options: {
        url: 'localhost:5000',
        package: 'sample',
        protoPath: '../proto/sample.proto',
      },
    }
  );
  await app.listen();
}
bootstrap();
// app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { GrpcMethod } from '@nestjs/microservices';
import { GetSampleRequest, Sample, SampleServiceController } from './gen_type/sample';

@Controller()
export class AppController implements SampleServiceController {
  constructor(private readonly appService: AppService) {}

  @GrpcMethod('SampleService')
  getSample(data: GetSampleRequest): Sample{
    
    return {
      id: 1,
      name: 'sample',
      type: 0,
      child: {
        id: 1,
        childName: 'child',
      },
      updatedAt: {
        seconds: Math.round(Date.now() / 1000),
        nanos : 0,
      },
      tags: ['tag1', 'tag2'],
    };
  }
}

この実装では、リクエストに関係なくSample型のデータが返されますが、ぜひGetSampleRequestの値を取り出すような実装にもチャレンジしてみてください。

自動生成された型を利用して快適にハンドリング可能です。

gRPCクライアントを利用して、gRPC通信を試してみる

https://engineering.mercari.com/blog/entry/grpc_and_evans/
https://github.com/ktr0731/evans

サクッとFlutterクライアントを作って叩いてみたいところですが、エラーが起こったりすると原因の切り分けが大変で無駄な時間を取られたりするので、CLIクライアントのevansを導入してみます。

evansを使うと、サーバーが展開しているサービスやメソッドの一覧を得ることができるため、非常に便利です。

evans --proto proto/sample.proto cli list #サービスの列挙
> sample.SampleService

evans --proto proto/sample.proto cli list sample.SampleService #メソッドの列挙
> sample.SampleService.GetSample

evans --proto proto/sample.proto cli desc sample.SampleService.GetSample # GetSampleメソッドの定義の表示
> sample.SampleService.GetSample:
> rpc GetSample ( .sample.GetSampleRequest ) returns ( .sample.Sample );

echo '{"id": "1" }' | evans --proto ../proto/sample.proto -p 5000 cli call sample.SampleService.GetSample
>{
>  "child": {
>    "childName": "child",
>    "id": 1
>  },
>  "id": 1,
>  "name": "sample",
>  "tags": [
>    "tag1",
>    "tag2"
>  ],
>  "updatedAt": "2023-08-05T00:00:17.332Z"
>}

しっかり値が返ってきましたね。

返ってこないときは、protoの相対パスがずれていないか、port指定が間違っていないか、メソッドの名称が間違っていないかなどを確認しましょう。

サーバーがきちんと動作していることが確認できたら、いよいよFlutter側の実装に入ります!

Dartで最も簡単なgRPC実装を行っていく

grpcとprotobufのインストール

flutter pub add grpc
flutter pub add protobuf

protoのDart plugin をインストールする

TypeScriptでもやったように、protoからDartのコードを生成する必要があります。

まずはDart用のプラグインをインストールしていきましょう!

// PATHが通っていない場合は通す
dart pub global activate protoc_plugin

protoでDartのコードを生成する

さっそくDartコードを自動生成していきます。

ディレクトリ構造は以下の通りです。

.
├── lib
│   ├── grpc_gen  //ここに生成
│   ├── provider
│   └── service
├── proto      // sample.protoを配置
├── server

生成コマンドは以下の通り。

同じprotocを使っていますが、引数が違うので注意しましょう。

// proto内でtimestampをimportしているので最後にgoogle/protobuf/timestamp.protoをつけています
protoc --dart_out=grpc:./lib/grpc_gen -I ./proto proto/sample.proto google/protobuf/timestamp.proto

これで、 grpc_gen ディレクトリに自動生成されたDartコードが出力されます。
もちろん、ほかのディレクトリに変更することも可能なので好きに変更してください。

必要なパッケージのインストール

サーバーとのチャンネルをFlutterのいろいろな場所から呼び出せたら便利なので、riverpodをインストールして利用していきます。

こちらの手順はgRPC Client を作るのに必須の手順ではありませんので取捨選択してください。

//これらはoptionなので必要な方のみ入れてください
flutter pub add hooks_riverpod
flutter pub add flutter_hooks

FlutterのgRPCクライアントを書く!

いよいよFlutterによるgRPCクライアントを書いていきます。

まずは channel を他のWidget内で呼び出せるように、 grpcChannelProvier を定義します。

grpc_channel_provider.dart
import 'package:grpc/grpc.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final grpcChannelProvider = Provider.autoDispose<ClientChannel>((ref) {
  final channel = ClientChannel(
    'localhost',
    port: 5000,
		// defaultでは.secure()
		// 自前の証明書の設定をすることも可能。
		// gRPCのセキュリティホールで一番多いのが、ここの設定漏れなので注意。
    options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
  );

  ref.onDispose(() {
    channel.shutdown();
  });

  return channel;
});

さらに、 channnel を受け取って、Server側のメソッドを呼び出すための GrpcSampleService クラスを定義しました。このクラスには、のちほど追加するStreamなメソッドもこのクラスに集約していく予定です。

grpc_sample_service.dart
import 'package:grpc/grpc.dart';
import 'package:grpc_mobile_server/grpc_gen/proto/sample.pbgrpc.dart';

class GrpcSampleService {
  static Future<Sample> getSample(ClientChannel channel, String id) async {
    final client = SampleServiceClient(channel);
    final res = await client.getSample(
      // セッターを使ってidをセットしている
      // 以前のバージョンでは、idを引数として渡せたが、
      // リリース時のバイナリサイズが増加するため削除された(21.0.0)
      // https://pub.dev/packages/protoc_plugin/changelog
      GetSampleRequest()..id = id,
    );;
    return res;
  }
}

https://pub.dev/packages/protoc_plugin/changelog
自動生成した型に対して、以前は名前付き引数を利用して値をセットすることができたらしいのですが、リリース時のバイナリサイズが増加してしまうとの理由で削除されています。

ちょっと不自然ですね。引数が多重ネストされて、メンバもめちゃくちゃ多いケースでは大変かなと思います。

ただし、実際書いていると、値をセットする際も定義された型の恩恵を受けられるため、そこまで大きな問題ではないかなと感じました。

FlutterからNestJS製のgRPCサーバーのメソッドを呼び出す

final res = await GrpcSampleService.getSample(channel, 'testId');
print(res);

//toLocal: trueとすれば、日本時間に変換できる
final date = res.updatedAt.toDateTime();
print(date);

呼び出し側のコード例は上記のような感じです。

gRPCで定義したTimeStampは、DartのDateTime型ではありませんが、自動生成により toDateTime() が生えるのでいい感じ。変換ミスなどを気にせずコードをかけるのが非常に良いです。

当たり前ですが、返り値の型はSample型として定義されているため、エディタの補完を受けることができ、開発者体験は非常に良いです。

ここまでで、gRPCの最もシンプルな通信が成立しました。
さらに、tokenを送り付けてサーバーで検証するような実装を考えてみます。

gRPCでtokenをどうやって送り付けるか考える

https://grpc.io/docs/what-is-grpc/core-concepts/#metadata
https://grpc.io/docs/guides/auth/
Basic認証でもBearer認証でも、基本的にはtokenなどの認証情報をどのようにサーバー側に送り付けるかさえクリアしてしまえば実装できそうです。

通常、認証情報はHTTPヘッダーにつけてサーバーに送っています。gRPCはHTTP/2を利用しているため、同じくヘッダーにつけたいところです。しかし、gRPCからHTTP/2のヘッダーになにかをつけることが難しいので、MetaDataという仕組みを使っていきます。

MetaDataで認証や認可を行うことはgRPCにおける標準的な実装です。
MetaDataにはDartの Map<String, String> 型で好きなデータを添付することができます。

もちろん、Server上でもMetaDataを簡単に検証可能なので、これを使っていきましょう。

FlutterからMetaDataをつけてサーバーに送る

送り付ける側はめっちゃ簡単です。

grpc_sample_service.dart
// 1.追加
static final options = CallOptions(metadata: {
  'authorization': 'Bearer token-hogehogefugafuga',
});

static Future<Sample> getSample(ClientChannel channel, String id) async {
  final client = SampleServiceClient(channel);
  final res = await client.getSample(
    GetSampleRequest()..id = id,
    
  // 2. 追加
  options: options,
  );

  return res;
}

自動生成されたgRPC Serviceのメソッドの第2引数に、 CallOption をつけるだけ。

認証認可にとどまらず、いろんなユースケースで使えそうですね。

NestJSサーバーでMetaDataを検証する

NestJSにはGuardという仕組みがあり、呼び出されたメソッドを呼び出す前に、処理を行っていいか検証する役割を果たします。

クライアント側から送られてきたMetaDataを検証してみましょう。

src/huard/auth.guard.ts
import { Metadata } from "@grpc/grpc-js";
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { RpcException } from "@nestjs/microservices";
import { Observable } from "rxjs";

@Injectable()
export class AuthGuard implements CanActivate { 
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        console.log('AuthGuard');
        const metaData = context.switchToRpc().getContext() as Metadata;
        const authToken = metaData.get('authorization')[0];

        try {
            // NOTE : ここで認証ロジックを入れる
            console.log('Auth Guard/metaData', authToken);

            // 認証に失敗したことを想定して、
            // エラーを返す場合は以下のコメントを外す
            // throw new Error();

            // metaDataをサーバーでセットして、Serviceで取得したりすることもできる            
            metaData.add('role', 'admin');

            // 認証成功の場合はtrueを返せばOK
            return true;

        } catch (e) {
            // 認証ロジックでエラーが発生した場合は、
            // UnauthorizedExceptionを返すことができる
            throw new RpcException({
                code: 16,
                message: 'unauthorized error. please check your authorization in metaData.',
            });
        }
     };
}

MetaDataを簡単に呼び出すことが可能です。

また、 Metadata クラスには .get() メソッドが生えているので、簡単にデータを取り出すことができます。

さらに、Guardの処理が終わった後に、NestJSのControllerやServiceで利用できるように .set() を利用して新たなMetaDataを加えることも可能です。例えば、JWTを検証してロールをつけるケースなどに有用です。

最後に、gRPCのStatus Codeは、一般的なHTTPのStatus Codeとは違うので注意しておきましょう。NestJSでは RpcException を利用します。

https://grpc.github.io/grpc/core/md_doc_statuscodes.html

Guardでセットした role というMetaDataをControllerで取り出す場合は以下のようにします。

server\src\app.controller.ts
@UseGuards(AuthGuard) // guardを使いたいのでアノテーションをつける
@GrpcMethod('SampleService')
getSample(data: GetSampleRequest, metadata: Metadata): Sample{
  console.log('data', data);
  console.log('metadata', metadata);
  console.log('role', metadata.get('role'));  // role [ 'admin' ]
  
// 省略
}

このように書いておけば role による処理の切り替えなども簡単に実装可能です。

Serverside Streamingを試してみる

ここからはServerside Streamingを試してみます。

先述の通り、クライアントから送った1回のリクエストに対し、複数のレスポンスをサーバーから返すタイプの通信方式です。

protoファイルでStreamを定義する

protoファイルに以下のように追加します

sample.proto
service SampleService {
    rpc GetSample (GetSampleRequest) returns (Sample) {}

    // 1.追加する
    rpc GetStreamingSample (GetSampleRequest) returns (stream StreamResult) {}
}

message GetSampleRequest {
    string id = 1;
}

// 2. 追加する
message StreamResult {
    int32 value = 1;
}

その後、先述した手順によってprotoをコンパイル、TypeScriptの定義ファイルを生成しましょう。

NestJSでServerside Streamingに対応する

今回は、ただ順番に数値をincrementしながら返すサンプルを作成しました。

この実装を変えていけば、なにかサーバー側の処理をして、それを待つような場合、モバイル側に処理状況を伝えることができます。

server\src\app.controller.ts
@GrpcMethod('SampleService')
getStreamingSample(request: GetSampleRequest, metadata: Metadata, ...rest: any): Observable<StreamResult> {
  console.log('streaming start');
  const subject = new Subject<StreamResult>();
  var value = 1;

  const onNext = async () => { 
    await new Promise(resolve => setTimeout(resolve, 10));

    if (value > 100) {
      subject.complete();
      return;
    }

    subject.next({
      value,
    });

    console.log('streaming', value);
    value++;
  };

  subject.subscribe({
    next: onNext,
    error: (e) => { new RpcException({ code: 4, message: e.message }) },
    complete() {
      subject.complete();
      console.log('completed');
    },
  });

  onNext();

  return subject.asObservable();
}

Flutter側でStreamを受信する

Dart側でもprotoから定義ファイルを自動生成したあと、 grpc_sample_service.dart に追記していきます。

lib\service\grpc_sample_service.dart
static Stream<StreamResult> getStreamingSample(ClientChannel channel) {
  final client = SampleServiceClient(channel);
  final request = GetSampleRequest()..id = 'id';
  
  return client.getStreamingSample(
    request,
    options: options,
  );
}

めっちゃ簡単です。

せっかくなので streamProvider を定義してUIに値を反映するようにしてみましょう。

final streamProvider = StreamProvider.autoDispose((ref) {
  final channel = ref.watch(grpcChannelProvider);
  return GrpcSampleService.getStreamingSample(channel);
}); 

UI側ではこんなコードを書けばOKです

final stream = ref.watch(streamProvider);

stream.when(
    error: (_, __) => const Text('error'),
    loading: () => const SizedBox(),
    data: (message) => CircularProgressIndicator(
          value: message.value.toDouble() / 100,
        ),
    ),

これだけでサーバーから値をとってインジケータを進めるような実装が完成しました!
実際にコードを動かして、サーバーから受け取った値でProgressIndicatorを動かしてみるとニヤッとできるはず。

結論、gRPCをFlutterフロント/サーバー通信に導入できそうか!?

実際に実装してみて、以下のように感じました

  • 複数言語間の連携は、protoによる自動生成により、かなり快適に実装できそう
  • Streamがかなり良い。UXを向上させる機能が簡単に実装できるのは素晴らしい
  • Dartの実装例は決して多くない

基本的な開発体験は、かなり良いです。
Streamも非常に簡単に書けますし、protoで生成した型にはjson stringを返すwriteToJson()や、Map<String,dynamic>を返すwriteToJsonMap()などの便利メソッドも生えたりするので、RESTful APIと混在していても比較的簡単に連携が取れるようになっています。

一方、リクエストの際にセッターを使わなければいけない点などは、Dartの開発においては少し不自然な構文を強制されるので好みがわかれるところかもしれません。

また、gRPCでは一般的なHTTPのステータスコードが利用されず、独自のエラーコードが返される点も注意が必要です。例えば、NestJSで実装されているUnauthorizedException()はそのまま利用することができず、サーバー側の実装については、ここが面倒なところかもしれません。

もちろんサーバー側で適切に実装していれば、Dart側では以下のようにGrpcErrorクラスを用いて検証もシンプルに行うことができます。

try {
  
  // grpcエラーを返す処理
  
} on GrpcError catch (e) {
  print(e.codeName);
  print(e.message);
}

DartにおけるgRPCの実装はGoなどの言語に比べて少ないため、バックエンド側の開発は向かないと思います。

一方、クライアント側では自動生成された型の恩恵を十分に受けることができます。
また、Streamの実装は非常に魅力的ですし、通信リソースの最適化という意味でもgRPCを採用する価値は十分にあると感じました。

また、BFFにおいてGraphQLやそのリゾルバを用意していくよりも、gRPCでシンプルにつないだアーキテクチャは実装がシンプルになると感じました。ブラウザがデフォルトでgRPCに対応していないため、Webサービスを統合する場合はモバイルとは違う状況であることも考慮すべきでしょう。

おまけ

https://twitter.com/hagakun_yakuzai

最近めっちゃ暑いですね...
暑すぎて、ThredsやBlueSkyのアカウントを作る余裕はありません。
ぜひTwitterで友達になりましょう。

株式会社マインディアでは、Flutterリードエンジニア、Railsエンジニアを募集しております。
カジュアル面談などの場を用意しておりますので、気軽にお声がけください。

引き続き、Flutter周りの気になることを調査したり、ジュニアなエンジニアが思うことも記事にしていきますので、良かったらZennTwitterのフォローをお願いします!

参考

https://zenn.dev/efx/articles/e90a93c1bd210e
https://qiita.com/kabochapo/items/6848457ea7a966baf957
https://rightcode.co.jp/blog/information-technology/flutter-grpc-api-1-syain

likeください!

株式会社マインディア テックブログ

Discussion