Розробка gRPC API для мобільного застосунку
gRPC — це не «швидкий REST». Це принципово інший підхід: бінарний протокол (Protocol Buffers), строго типізований контракт через .proto файли, вбудована streaming, кодогенерація клієнтів для будь-якої платформи. Для мобільних gRPC додає вагомі плюси (компактний payload, типобезопасність, bidirectional streaming) та специфічні складності (обмеження HTTP/2, відладка бінарних даних).
Коли gRPC на мобільному оправдан
gRPC перевищує REST/JSON у сценаріях:
- Високочастотні запити з передбачуваною схемою (трекінг, телеметрія, real-time дані)
- Streaming: одне довге з'єднання замість polling
- Мікросервісна архітектура, де mobile gateway вже на gRPC
JSON-over-REST простіше в відладці, більше бібліотек, немає залежності від HTTP/2. Для стандартного CRUD-застосунку gRPC надлишковий.
Визначення API через .proto
Контракт — єдиний джерело правди. Зміна .proto файлу приводить до перегенерації клієнтів на всіх платформах:
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 в ListProducts — server-side streaming: сервер відправляє продукти по одному по мірі готовності, не чекає все. WatchInventory — server-side stream для real-time оновлень.
Android: gRPC-Java / gRPC-Kotlin
// build.gradle
implementation 'io.grpc:grpc-android:1.x.x'
implementation 'io.grpc:grpc-okhttp:1.x.x' // транспорт через OkHttp (Android)
OkHttp-транспорт переважніше Netty на Android — Netty збільшує APK на ~3 МБ та повільніше ініціалізується.
val channel = ManagedChannelBuilder
.forAddress("api.example.com", 443)
.useTransportSecurity() // TLS обов'язково у production
.intercept(AuthInterceptor(tokenProvider))
.build()
val stub = ProductServiceGrpcKt.ProductServiceCoroutineStub(channel)
// Unary вызов
val product = stub.getProduct(getProductRequest { id = productId })
// Server streaming
stub.listProducts(listProductsRequest { categoryId = "electronics" })
.collect { product ->
// кожен product приходить по мірі відправки сервером
}
AuthInterceptor реалізує ClientInterceptor — додає Authorization метаданих до кожного вызова:
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 підтримує gRPC через пакет grpc-swift:
// Package.swift
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.0.0")
// Створення каналу
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 вызов
let request = ProductService_GetProductRequest.with { $0.id = productId }
let response = try await client.getProduct(request).response.get()
Server streaming через AsyncStream у Swift Concurrency:
let call = client.listProducts(listRequest)
for try await product in call.responses {
// обробляємо кожен продукт
}
HTTP/2 та мобільні проблеми
gRPC вимагає HTTP/2. Це дає мультиплексування (кілька запитів на одному TCP з'єднанні), але створює проблеми:
Intermediary proxies. Багато корпоративних проксі та деякі мобільні оператори не повністю підтримують HTTP/2 або його terminateHttp/2. Рішення — gRPC-Web (HTTP/1.1 транспорт через Envoy прокси), але втрачаємо streaming можливості.
Connection keepalive. gRPC-Android підтримує keepalive pings для збереження з'єднання через NAT:
ManagedChannelBuilder.forAddress(host, port)
.keepAliveTime(30, TimeUnit.SECONDS)
.keepAliveTimeout(10, TimeUnit.SECONDS)
.keepAliveWithoutCalls(false)
Deadline. Кожен gRPC вызов має мати deadline — інакше зависший запит висить вічно:
stub.withDeadlineAfter(10, TimeUnit.SECONDS).getProduct(request)
Отладка
JSON читається в Charles/Proxyman, Protocol Buffers — ні. Інструменти: grpcurl (command-line), grpc-ui (web UI), Wireshark з gRPC dissector. У dev-збірці логуємо через LoggingClientInterceptor з grpc-java.
Protobuf еволюція схеми
Зворотна сумісність — ключова перевага Protobuf. Правила:
- Нові поля додаємо з новим номером, ніколи не переиспользуємо видалені номери
-
requiredне використовуємо (Protobuf 3 видалив його не випадково) - Для перейменування: додаємо нове поле, помічаємо старе як
reserved
Порушення цих правил — binary incompatibility: старі версії застосунку будуть падати при отримуванні нової схеми.
Що входить у роботу
Проектуємо .proto схему, налаштовуємо кодогенерацію в CI (protoc плагіни), реалізуємо gRPC клієнт на Android/iOS з TLS, auth interceptor, deadline та retry policy, налаштовуємо отладочну інфраструктуру.
Строк: 2–4 тижні з урахуванням серверної частини та тестування на різних сітьових умовах.







