gRPC offers type-safe, efficient communication via Protocol Buffers over HTTP/2. On Android it's worth the setup cost when you need bidirectional streaming, strong typing across teams, or significant payload size reduction.
Protocol Buffers: Define Your API
// user.proto
syntax = "proto3";
package com.example;
option java_multiple_files = true;
option java_package = "com.example.grpc";
message UserRequest {
string user_id = 1;
}
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
}
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
rpc StreamUserUpdates (UserRequest) returns (stream UserResponse);
}
Gradle Setup
// build.gradle.kts (module)
plugins {
id("com.google.protobuf") version "0.9.4"
}
dependencies {
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
implementation("io.grpc:grpc-okhttp:1.62.2") // transport for Android
implementation("com.google.protobuf:protobuf-kotlin:3.25.3")
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.3" }
plugins {
create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.62.2" }
create("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" }
}
generateProtoTasks {
all().forEach { task ->
task.plugins {
create("grpc")
create("grpckt")
}
task.builtins { create("kotlin") }
}
}
}
Creating a Channel & Stub
class GrpcUserRepository(private val host: String, private val port: Int) {
private val channel: ManagedChannel = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext() // ⚠️ TLS only in production; use .useTransportSecurity()
.build()
private val stub = UserServiceGrpcKt.UserServiceCoroutineStub(channel)
// Unary call — one request, one response
suspend fun getUser(userId: String): UserResponse {
val request = userRequest { this.userId = userId }
return stub.getUser(request)
}
// Server streaming — one request, stream of responses
fun streamUserUpdates(userId: String): Flow<UserResponse> {
val request = userRequest { this.userId = userId }
return stub.streamUserUpdates(request)
}
fun shutdown() = channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)
}
Auth Interceptor
class AuthInterceptor(private val tokenProvider: () -> String) : ClientInterceptor {
override fun <Req, Resp> interceptCall(
method: MethodDescriptor<Req, Resp>,
callOptions: CallOptions,
next: Channel
): ClientCall<Req, Resp> {
return object : ForwardingClientCall.SimpleForwardingClientCall<Req, Resp>(
next.newCall(method, callOptions)
) {
override fun start(responseListener: Listener<Resp>, headers: Metadata) {
headers.put(
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER),
"Bearer ${tokenProvider()}"
)
super.start(responseListener, headers)
}
}
}
}
// Apply to channel
val channel = ManagedChannelBuilder
.forAddress(host, port)
.intercept(AuthInterceptor { tokenStore.getToken() })
.build()
Error Handling
gRPC errors come as StatusException:
try {
val user = stub.getUser(request)
} catch (e: StatusException) {
when (e.status.code) {
Status.Code.NOT_FOUND -> showError("User not found")
Status.Code.UNAUTHENTICATED -> refreshTokenAndRetry()
Status.Code.UNAVAILABLE -> showOfflineError()
else -> showGenericError(e.message)
}
}
gRPC vs REST Trade-offs
| Factor | gRPC | REST/JSON |
|---|---|---|
| Payload size | ~30–40% smaller (binary proto) | Larger (text JSON) |
| Streaming | Native (server/client/bidirectional) | Polling or WebSocket |
| Type safety | Compile-time via proto | Runtime |
| Tooling | Less ecosystem, harder to debug | Excellent (Postman, curl) |
| Browser support | Requires gRPC-Web proxy | Native |
| Team size | Better for large teams with shared protos | Fine for small teams |
Use gRPC when: multiple platforms share proto definitions, you need bidirectional streaming, or payload size is critical (IoT, high-frequency updates).
Use REST when: you want easy debugging, public-facing APIs, or browser clients without a proxy.