Developing gRPC API for Mobile Application
gRPC isn't "fast REST". It's fundamentally different: binary protocol (Protocol Buffers), strictly typed contract via .proto files, built-in streaming, client codegen for any platform. For mobile, gRPC adds significant advantages (compact payload, type safety, bidirectional streaming) and specific complexities (HTTP/2 limitations, binary data debugging).
When gRPC on Mobile Justified
gRPC wins over REST/JSON in scenarios:
- High-frequency requests with predictable schema (tracking, telemetry, real-time data)
- Streaming: one long connection instead of polling
- Microservice architecture where mobile gateway already on gRPC
JSON-over-REST is simpler to debug, more libraries, no HTTP/2 dependency. For standard CRUD app, gRPC is overkill.
Defining API via .proto
Contract — single source of truth. Changing .proto file triggers client regeneration on all platforms:
syntax = "proto3";
package mobile.api.v1;
service ProductService {
rpc GetProduct (GetProductRequest) returns (Product);
rpc ListProducts (ListProductsRequest) returns (stream Product);
rpc WatchInventory (WatchInventoryRequest) returns (stream InventoryUpdate);
}
message Product {
string id = 1;
string name = 2;
int64 price_cents = 3;
repeated string image_urls = 4;
}
message GetProductRequest {
string id = 1;
}
stream Product in ListProducts — server-side streaming: server sends products one by one as ready, doesn't wait. WatchInventory — server-side stream for real-time updates.
Android: gRPC-Java / gRPC-Kotlin
// build.gradle
implementation 'io.grpc:grpc-android:1.x.x'
implementation 'io.grpc:grpc-okhttp:1.x.x' // transport via OkHttp (Android)
OkHttp transport preferable to Netty on Android — Netty adds ~3 MB to APK and slower init.
val channel = ManagedChannelBuilder
.forAddress("api.example.com", 443)
.useTransportSecurity() // TLS mandatory in production
.intercept(AuthInterceptor(tokenProvider))
.build()
val stub = ProductServiceGrpcKt.ProductServiceCoroutineStub(channel)
// Unary call
val product = stub.getProduct(getProductRequest { id = productId })
// Server streaming
stub.listProducts(listProductsRequest { categoryId = "electronics" })
.collect { product ->
// each product arrives as server sends it
}
AuthInterceptor implements ClientInterceptor — adds Authorization metadata to each call:
class AuthInterceptor(private val tokenProvider: TokenProvider) : ClientInterceptor {
override fun <ReqT, RespT> interceptCall(
method: MethodDescriptor<ReqT, RespT>,
callOptions: CallOptions,
next: Channel
): ClientCall<ReqT, RespT> {
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, callOptions)
) {
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
headers.put(
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
"Bearer ${tokenProvider.getToken()}"
)
super.start(responseListener, headers)
}
}
}
}
iOS: gRPC-Swift
Apple supports gRPC via grpc-swift package:
// Package.swift
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.0.0")
// Channel creation
let group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
let channel = try GRPCChannelPool.with(
target: .host("api.example.com", port: 443),
transportSecurity: .tls(.makeClientDefault(compatibleWith: group)),
eventLoopGroup: group
)
let client = ProductService_ProductServiceNIOClient(channel: channel)
// Unary call
let request = ProductService_GetProductRequest.with { $0.id = productId }
let response = try await client.getProduct(request).response.get()
Server streaming via AsyncStream in Swift Concurrency:
let call = client.listProducts(listRequest)
for try await product in call.responses {
// process each product
}
HTTP/2 and Mobile Issues
gRPC requires HTTP/2. Brings multiplexing (multiple requests on one TCP), but creates issues:
Intermediary proxies. Many corporate proxies and some mobile operators don't fully support HTTP/2 or terminate it. Solution — gRPC-Web (HTTP/1.1 transport via Envoy proxy), but lose streaming.
Connection keepalive. gRPC-Android supports keepalive pings to maintain connection through NAT:
ManagedChannelBuilder.forAddress(host, port)
.keepAliveTime(30, TimeUnit.SECONDS)
.keepAliveTimeout(10, TimeUnit.SECONDS)
.keepAliveWithoutCalls(false)
Deadline. Each gRPC call should have deadline — otherwise hung request hangs forever:
stub.withDeadlineAfter(10, TimeUnit.SECONDS).getProduct(request)
Debugging
JSON readable in Charles/Proxyman, Protocol Buffers — no. Tools: grpcurl (command-line), grpc-ui (web UI), Wireshark with gRPC dissector. In dev build log via LoggingClientInterceptor from grpc-java.
Protobuf Schema Evolution
Backward compatibility — key Protobuf advantage. Rules:
- Add new fields with new number, never reuse deleted numbers
- Don't use
required(Protobuf 3 removed for reason) - For rename: add new field, mark old as
reserved
Breaking these causes binary incompatibility: old app versions crash on new schema.
What's Included
Design .proto schema, configure codegen in CI (protoc plugins), implement gRPC client on Android/iOS with TLS, auth interceptor, deadline and retry policy, setup debugging infrastructure.
Timeline: 2–4 weeks including server part and testing on various network conditions.







