サーバ間、サーバとクライアント間の通信で gRPCを採用することは、様々なメリットがあります。
AndroidでもgRPCの採用事例が増えてきたように思います。
以前はgrpc/grpc-javaを使ってstubを生成してたと思うのですが、いつの間にかgrpc/grpc-kotlinも出ていました。
しっかりとkotlin coroutinesをサポートしており、かなり便利だったので、導入方法を紹介します。
まずはproto fileを作りましょう。
どこでも良さそうですが、app/src/main/proto
に入れています。
java_package
は指定しておいたほうがわかりやすいと思います。
検証のため、stream系のrpcも一通り揃えておきました。
// app/src/main/proto/test.proto
syntax = "proto3";
package test;
option java_package = "com.example.test.pb";
service TestService {
rpc SayHello(HelloRequest) returns (HelloResponse);
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
}
message HelloRequest {
string message = 1;
}
message HelloResponse {
string message = 1;
}
コード生成もgrdle pluginで行います。
まず、rootのbuild.gradleにpluginを追加
// build.gradle
buildscript {
...
dependencies {
...
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
}
}
appのbuild.gradleにapply pluginを追加。
// app/build.gradle
apply plugin: 'com.google.protobuf'
...
更に、依存関係を追加。
// app/build.gradle
...
dependencies {
...
implementation "io.grpc:grpc-okhttp:1.29.0"
implementation "io.grpc:grpc-protobuf:1.29.0"
implementation "io.grpc:grpc-stub:1.29.0"
implementation "io.grpc:grpc-kotlin-stub:0.1.1"
implementation "javax.annotation:javax.annotation-api:1.3.2"
}
最後にコード生成用のpluginを追加。
android等で使う場合は、liteのoptionを入れておいたほうが良さそうです。
// app/build.gradle
...
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.11.0' }
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.29.0'
}
grpckt {
artifact = 'io.grpc:protoc-gen-grpc-kotlin:0.1.1'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
task.plugins {
grpc { option 'lite' }
grpckt {}
}
}
}
}
app/src/main/proto
以外にproto fileを置いている場合は、別途フォルダ指定が必要になります。
// app/build.gradle
...
android {
...
sourceSets {
main {
proto {
srcDir 'src/main/protobuf'
}
}
}
}
これで、一度buildすると、app/build/generated/source/proto/debug/grpckt/
あたりにコードが生成されているはずです。
まず、チャンネルを作成します。
private const val HOST = "127.0.0.1"
private const val PORT = 8080
val channel = ManagedChannelBuilder
.forAddress(HOST, PORT)
.usePlaintext()
.build()
それを使って、TestServiceCoroutineStub
のインスタンスを作成します。
val testService = TestServiceGrpcKt
.TestServiceCoroutineStub(channel)
1で定義したrpcはそれぞれ以下のようなinterfaceになっており、それぞれkotlin coroutinesを使って呼び出すことができます。
streamがちゃんとflowになっているところが、親切ですね。
class TestServiceCoroutineStub {
suspend fun sayHello(request: Test.HelloRequest): Test.HelloResponse { ... }
fun lotsOfReplies(request: Test.HelloRequest): Flow<Test.HelloResponse> { ... }
suspend fun lotsOfGreetings(requests: Flow<Test.HelloRequest>): Test.HelloResponse { ... }
fun bidiHello(requests: Flow<Test.HelloRequest>): Flow<Test.HelloResponse> { ... }
}
viewModelScope等を使って呼び出しましょう。
class TestViewModel: ViewModel() {
private val testSerivce: TestServiceCoroutineStub = ...
init {
viewModelScope.launch {
val request = Test.HelloRequest.newBuilder()
.setMessage("hello")
.build()
val response = testSerivce.sayHello(request)
...
}
}
}
grpc-javaではblockingStubやasyncStubを生成することができますが、kotlin coroutinesを採用しているプロジェクトであれば、いちいちmapperを書かなければならず、少しめんどくさかったように思います。
そこが、最初からsuspend functionやflowを使った形で生成してくれるので、非常に便利になりましたね。
一方で、あくまでもgrpc-javaのwrapperなので、kotlin native等ではまだ使うことができなさそうですね。
requestのbuilderも冗長に感じますし、one of等もsealed class等を使ってもっとシンプルに表現できそうな気がします。
今後の発展にも注目です。