ハトネコエ Web がくしゅうちょう

プログラミングやサーバー関連、チームマネジメントなど得た技術のまとめ

AndroidのgRPCクライアントをKotlinで実装しました

このあいだの記事では、Go 言語による gRPC サーバーの実装を完了させました。
また、 Go 言語によるクライアント実装についても触れました。

実運用としてクライアント(データの受け取り手)となるものは、Web の他に、
AndroidiOS などのスマートフォンであることが多いかと思います。

というわけで、今回は、Android の gRPC クライアントKotlin で実装してみました。

gRPC Client for Android written in Kotlin
gRPC Client for Android written in Kotlin

1. 参考

以下が特に参考になりました。

また、出来上がりがこちらですので、
このあとの説明は、このコードを見ながらだとわかりやすいと思います。

2. proto を ビルドするための gradle の設定

最近は build.gradle を Groovy でなく Kotlin で書くことも流行っている気配なので Kotlin で書きました。
……が、そこまで大きなメリットがあるわけではなく、
少数派の書き方となってしまい情報が得づらくなるので、Android 開発に慣れていなければあまりオススメできません。

基本的には app ディレクトリ以下の build.gradle.kts を編集すれば良く、
grpc-java リポジトリにある実装例の build.gradle を、Kotlin 向けに書き直して以下のようにしました。

import com.google.protobuf.gradle.*
import org.jetbrains.kotlin.config.KotlinCompilerVersion

plugins {
    id("com.android.application")
    kotlin("android")
    kotlin("android.extensions")

    id("com.google.protobuf") version "0.8.8"
}

val grpcVersion = "1.18.0"
val protocVersion = "3.6.1"

android {
    compileSdkVersion(28)
    defaultConfig {
        applicationId = "net.hatone.hello_grpc"
        minSdkVersion(21)
        targetSdkVersion(28)
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        getByName("debug") {
            isMinifyEnabled = false
        }
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    implementation(kotlin("stdlib-jdk8", KotlinCompilerVersion.VERSION))
    implementation("com.android.support:appcompat-v7:28.0.0")
    implementation("com.android.support.constraint:constraint-layout:1.1.3")

    // Test
    testImplementation("junit:junit:4.12")
    androidTestImplementation("com.android.support.test:runner:1.0.2")
    androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")

    // gRPC
    implementation("io.grpc:grpc-okhttp:$grpcVersion")
    implementation("io.grpc:grpc-protobuf-lite:$grpcVersion")
    implementation("io.grpc:grpc-stub:$grpcVersion")
    implementation("javax.annotation:javax.annotation-api:1.3.2")
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:$protocVersion"
    }

    plugins {
        id("javalite") {
            artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0"
        }
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion"
        }
    }

    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("javalite")
                id("grpc") {
                    option("lite")
                }
            }
        }
    }
}

https://github.com/nekonenene/grpc_android_client/blob/master/app/build.gradle.kts

ポイントは、plugins ブロックにある id("com.google.protobuf") version "0.8.8" です。
これにより、 protobuf-gradle-plugin が依存関係となり、
protobuf ブロックの使用が可能になっています。

なお、『API 'variant.getJavaCompile()' is obsolete and has been replaced with 'variant.getJavaCompileProvider()'.』という Warning がビルド時に出ますが、
おそらくこれは protobuf-gradle-plugin の問題によるもので、近い将来解決されるでしょう。

proto ファイルは app/src/main/proto ディレクトリ以下に置きます。
それ以外のディレクトリに置きたい場合は、 sourceSets ブロックに定義を書きましょう。
(参考: https://github.com/google/protobuf-gradle-plugin#customizing-source-directories

ビルドに成功すると、以下のように generatedJava フォルダの下に、
proto ファイルの定義に従って Java のクラスやインターフェイスが作られたことが確認できます。

f:id:nekonenene:20190222093401p:plain

generatedJava は便宜上のフォルダで、実際は
GreeterGrpc.javaapp/build/generated/source/proto/debug/grpc/io/grpc/examples/hello/ ディレクトリ下、
HelloProto.java などは ⁨app/build/generated/source/proto/debug/javalite/io/grpc/examples/hello/ ディレクトリ下に作られていました。

3. gRPC 通信をおこなう

これもまた、grpc-java リポジトリにある実装例の HelloworldActivity.java を Kotlin 向けに書き直せばいけます。
……が、このままだとさすがに汚いので、このコードで言うところの GrpcTask クラスは別ファイルに分け、
また、callback を設定し、Activity と疎結合になるようにしました。

https://github.com/nekonenene/grpc_android_client/tree/master/app/src/main/java/net/hatone/hello_grpc

やっていることとしては非常に単純で、

val channel = ManagedChannelBuilder
    .forAddress("grpc-test.hatone.net", 443)
    .useTransportSecurity()
    .build()
val stub = GreeterGrpc.newBlockingStub(channel)
val request = HelloRequest.newBuilder()
    .setName("Nanako")
    .setAge(75)
    .build()
val reply: HelloReply = stub.sayHello(request)
reply.message

このような書き方で簡単にレスポンスを取得できてしまいます。
GreeterGrpc クラスなどが自動生成されてくれるおかげで楽ができるのです。

SSL/TLS 通信をおこなうので、ManagedChannel 作成の際に
usePlaintext() でなく useTransportSecurity() メソッドを使うようになっている点にご注意ください。

また、インターネット通信をおこなうので、 AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

の1行を追加しておくことを忘れないようにしてください。

4. おわりに

もうちょっと書くことがある気もするんですが、ブログにしてみると意外とこんなものでした。

proto ファイルは今回は gRPC サーバー側の hello.proto を単純にコピーしてきましたが、
同一である必要があるので、別リポジトリを作って管理し、
submodule として各々が最新を引っ張ってくる作りにしておくと良いと思います。

久しぶりに Android アプリのコードを書いたので何度か悩みましたが、
それなりにきれいな実装に出来たと思うのでよかったです。
今回は AsyncTask を使いましたが、 RxJavaRxKotlin)も使ってみたいですね!