🆙

Android へ Connect RPC を導入

に公開

こんにちは。ビジネス映像メディア「PIVOT」で、Androidエンジニアをしている さきさん です。

この記事では、PIVOTのプロダクトチームで新たに導入した Connect RPCの Androidアプリへの導入事例をまとめます。

Connect RPC導入の背景

PIVOTアプリではもともと、現在は、REST API のレスポンスデータを Protocol Buffers(以下 Proto)でシリアライズ して返却する方式を採用していました。それにより各プラットフォーム間での「API レスポンスを効率的に圧縮する」ことが可能となっていました。

しかし、日々の開発・運用において。以下のような課題が見えてきました。

  • レスポンス側のみでの Proto 利用
    通信の片側(レスポンス)だけで Proto を使っており、完全なスキーマ駆動設計になっていない。

  • RPC 化されていない
    gRPC のようなリクエスト/レスポンスの型安全な通信ではなく、あくまでデータ効率を高めるための部分利用にとどまっている。

  • スキーマからのコード生成が未活用
    .proto ファイルを元に通信層のコードを自動生成しておらず、開発・保守の効率化が十分に図れていない。

そのカウンターとして導入したのが Connect RPCです。

Androidアプリ開発における Connect RPCの恩恵

https://connectrpc.com/

Androidアプリで gRPCを利用する場合、通信ライブラリの依存や HTTP/2環境の制約がネックになりがちです。
バイナリ形式のためデバッグもしづらく、通信ログも解析しにくい。

しかし、Connect RPC は、そんな課題を解決するために設計された新しい RPCフレームワークです。
Protobuf スキーマを共有しつつ、HTTP/1.1/2 両対応・JSON 通信も可能なため、通常のHTTP API と同じように扱えます。

つまり「gRPCの信頼性」と「RESTの扱いやすさ」を両立した仕組みと言えます。

実際に Connect RPC をAndroid側で導入する際、一般的な OkHttpや Retrofit といった通信ライブラリへも簡単に組み込むことが可能となっているため導入も用意、かつビルドやデバッグのコストを抑えつつ、型安全な通信を実現できます。

それにより

  • サーバー/クライアント間のAPI整合性が保たれ、「モバイル・バックエンド・フロントエンドで異なるAPI仕様が存在する」や型安全性によるヒューマンエラーからの不具合やクラッシュを防止できます
  • Retrofitのような感覚でRPC呼び出しが可能となり、gRPC特有の複雑なChannelやStub管理を意識せずにリクエストが可能

結果として、バックエンド・クライアント間の開発速度が向上し、サービス全体の拡張性と透明性が高まりました。

PIVOTでのConnect RPCの導入の経緯や詳細は、@tawachan が、以下の記事でもまとめてくれているのでよければご一読ください。

https://zenn.dev/pivotmedia/articles/pivot-connect-rpc-adoption?q=1

Androidでの Connect RPCの導入方法

依存関係を宣言

app/build.gradle.kts

android {
   ...
    
   dependencies {
      // Protobuf Lite
      implementation(libs.protobufJavalite)
      // Connect RPC
      implementation(libs.connect.kotlin)
      implementation(libs.connect.kotlin.okhttp)
      implementation(libs.connect.kotlin.google.javalite.ext)
    }
  }

  ...

  protobuf {
    protoc { artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}" }
    generateProtoTasks {
      all().forEach { task ->
        task.builtins { id("java") { option("lite") } }
      }
    }
  }

Connectクライアントの生成

OkHttpにConnectOkHttpClientをラップし、Connectプロトコル、Protobuf Lite戦略を指定してProtocolClientを生成しています。
認証やアプリバージョン等のヘッダはOkHttpインターセプタで付与します。

  private fun generateProtocolClient(host: String, cache: Cache): ProtocolClient =
    ProtocolClient(
      httpClient = ConnectOkHttpClient(generateConnectOkHttpClient(cache)),
      config = ProtocolClientConfig(
        host = host,
        serializationStrategy = GoogleJavaLiteProtobufStrategy(),
        networkProtocol = NetworkProtocol.CONNECT,
      ),
    )

  private fun generateConnectOkHttpClient(cache: Cache): OkHttpClient =
    OkHttpClient.Builder()
      .socketFactory(NoDelaySocketFactory())
      .cache(cache)
      .addInterceptor(AuthInterceptor())           // 認証・メタ情報
      .addNetworkInterceptor(NetworkInterceptor()) // ログ
      .build()

アプリ起動時にサービスのクライアントを初期化生成

val serviceClient = createServiceClient(host = "$host", Cache())

APIの呼び出し

 // APIごとにリクエスト・レスポンスクラスが自動生成されるため、型やパラメータの不一致を防げる
 private suspend fun ping(): ResponseMessage<PingResponse> =
    serviceClient.ping(
      request = PingRequest
        .newBuilder()
        .setPalameter(...)
        .build(),
    )

最後に

Connect RPCのおかげで、サーバーとクライアントの距離がぐっと近くなりました。
これからも、開発体験を良くするための仕組みを積極的に取り入れていきたいと思います。

PIVOT Tech Blog

Discussion