How to implement gRPC in Android?

Pratik Bhagwatkar
Globant
Published in
6 min readAug 24, 2023
source : https://unsplash.com/

Co-author: Ashish Mishra

In this article, we are going to see how we can integrate the gRPC service into an Android Project.

If you are new to gRPC and want to learn more about it, you can check out this article.

gRPC Android Client Communication.

Before getting to the details of the implementation. Let’s go through the different RPC methods supported by the gRPC framework:

  1. Unary RPC: The client sends a single request to the server and receives a single response.
  2. Server Streaming RPC: The client sends a request to the server. Get a stream to read a sequence of messages back.
  3. Client Streaming RPC: Using a given stream, the client writes a sequence of messages and delivers them to the server.
  4. Bidirectional Streaming RPC: Both server and client send a sequence of messages using a read-write stream.

Getting Started

Let’s get started with one example. We require two things to integrate gRPC into our project.:

  • protobuf .proto to create our .pb and .grpc java files.
  • grpc-kotlin library to establish a connection with gRPC.

Let’s understand both the above points in detail.

The protobuf is a generic definition file. It contains all the descriptions of the services we are calling. The requests we can make, and the response model we receive from the services.

First, we´ll create a project in Android Studio.

New Project in Android Studio

Here you can see a new Android Studio project called CalcGRPC being created. This project does some mathematical operations.

After creating the project, we’re going to configure our project-level build.gradle file.

buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.19")
}
}

We’re using the protobuf Gradle plugin to generate our gRPC and protobuf libraries. Whenever we compile, it will generate them.

After configuring the project-level gradle file. We will add the required plugins and libraries in the app/build.gradle file.

plugins {
id 'com.google.protobuf'
}
dependencies {
implementation 'com.google.protobuf:protobuf-lite:3.0.1'
implementation 'io.grpc:grpc-okhttp:1.32.2'
implementation 'io.grpc:grpc-protobuf-lite:1.25.0'
implementation 'io.grpc:grpc-stub:1.25.0'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
}

We’ll next instruct Gradle on how to build the protobuf.

protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.10.0' }
plugins {
javalite { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" }
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.25.0'
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
javalite {}
grpc {
option 'lite'
}
}
}
}
}

After setting all the dependencies, we will now add .proto file to our project.

Path to store .proto file in the project.
src -> main -> proto -> calc.proto

syntax = "proto3";
package calc;
option java_package="com.example.calcgrpc";
option java_outer_classname="CalcProto";

service Calc {
rpc Add (AddRequest) returns (AddResponse) {};
rpc Fibo (FiboRequest) returns (stream FiboResponse) {};
rpc ComputeAverage (stream ComputeAverageRequest) returns (ComputeAverageResponse){};
rpc FindMaximum (stream FindMaximumRequest) returns (stream FindMaximumResponse){};
}

message AddRequest {
int32 first_number = 1;
int32 second_number = 2;
}
message AddResponse {
int32 sum_result = 1;
}
message DivReply {
int64 quotient = 1;
int64 remainder = 2;
}
message FiboRequest {
int64 num = 1;
}
message FiboResponse {
int64 num = 1;
}
message ComputeAverageResponse {
double average = 1;
}
message ComputeAverageRequest {
int32 number = 1;
}
message FindMaximumRequest {
int32 number = 1;
}
message FindMaximumResponse {
int32 maximum = 1;
}

The above calc.proto file consists of 3 things:

  • The RPC methods we are invoking from the client.
  • The request params we send along with the RPC requests.
  • The response we expect from these requests.

Now that we have our protobuf file ready, we need to generate Java source files out of thecalc.proto file. For that, rebuild the project, and it will generate CalcGrpc.java and CalcProto.java files.

Automatically generated Java files

Finally, we will see how to call all the service methods from our application.

First, we need to create a channel and stub:

Channel

A gRPC channel enables a connection to a gRPC server on a certain host and port. It is used when creating a client stub.

Stub

To call service methods, we first need to create stubs:

  • Synchronous or blocking stub: In this, the RPC call waits for the server to respond, and it will return a response or raise an exception.
  • Asynchronous or non-blocking stub: This sends non-blocking requests to the server, receiving an asynchronous answer in return. You can make certain types of streaming calls only using the asynchronous stub.
private val uri by lazy { Uri.parse(Constants.GRPC_BASE_URL) }
private var managedChannel =
ManagedChannelBuilder.forAddress(uri.host, uri.port).usePlaintext().build()
private var newBlockingStub = CalcGrpc.newBlockingStub(managedChannel)
private var newStub = CalcGrpc.newStub(managedChannel)

Now by using this, we can make calls to proto file methods.

  1. Unary RPC:
private fun invokeAdd() {
val createAddRequest =
CalcProto.AddRequest.newBuilder()
.setFirstNumber(2)
.setSecondNumber(44)
.build()
lifecycleScope.launch {
try {
val response = async { newBlockingStub.add(createAddRequest) }
val createAddResp = response.await()
tv_result.text = "Addition Result : ${createAddResp.sumResult}"
println("Addition result received : ${createAddResp.sumResult}")
} catch (e: Exception) {
e.printStackTrace()
}
}
}

Output received of Unary RPC from server:

The output of Unary RPC.

2. Server Streaming RPC:

 private fun invokeFiboStream() {
lifecycleScope.launch {
try {
val createFiboRequest =
CalcProto.FiboRequest.newBuilder().setNum(8).build()

newStub.fibo(createFiboRequest,
object : StreamObserver<CalcProto.FiboResponse> {
override fun onNext(value: CalcProto.FiboResponse) {
try {
runOnUiThread {
tv_result.text = "Fibo Stream Result : ${value.num}"
println("Fibo Stream Result : ${value.num}")
}
} catch (e: Exception) {
}
}
override fun onError(t: Throwable) {
}

override fun onCompleted() {
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
}

Output received of Server Streaming RPC from server:

The output of Server Streaming RPC

3. Client Streaming RPC:

private fun invokeCalculateAverage() {
lifecycleScope.launch {
try {
val call: StreamObserver<CalcProto.ComputeAverageRequest> =
newStub.computeAverage(object :
StreamObserver<CalcProto.ComputeAverageResponse> {
override fun onNext(value: CalcProto.ComputeAverageResponse) {
try {
runOnUiThread {
tv_result.text = "Average Result : ${value.average}"
println("Average Result : ${value.average}")
}
} catch (e: Exception) {
println("responseObserver Exception --> $e")
}
}

override fun onError(t: Throwable) {
println("responseObserver onError --> $t")
}

override fun onCompleted() {
println("responseObserver onNext --> onCompleted")

}
})
val createAvgRequest1 =
CalcProto.ComputeAverageRequest.newBuilder().setNumber(2).build()
call.onNext(createAvgRequest1)

val createAvgRequest2 =
CalcProto.ComputeAverageRequest.newBuilder().setNumber(4).build()
call.onNext(createAvgRequest2)

val createAvgRequest3 =
CalcProto.ComputeAverageRequest.newBuilder().setNumber(6).build()
call.onNext(createAvgRequest3)

call.onCompleted()

} catch (e: Exception) {
e.printStackTrace()
}
}
}

Output received of Client Streaming RPC from server:

The output of Client Streaming RPC

4. Bidirectional Streaming RPC:

private fun invokeBiDiFindMaximum() {
lifecycleScope.launch {
try {

val dataArr = listOf(3, 5, 17, 769, 8, 30, 12, 345, 129, 990)

val call: StreamObserver<CalcProto.FindMaximumRequest> =
newStub.findMaximum(object :
StreamObserver<CalcProto.FindMaximumResponse> {
override fun onNext(value: CalcProto.FindMaximumResponse) {
try {
runOnUiThread {
tv_result.text = "Maximum Stream Result : ${value.maximum}"
println("Maximum Stream Result : ${value.maximum}")
}
} catch (e: Exception) {
println("responseObserver Exception --> $e")
}
}

override fun onError(t: Throwable) {
println("responseObserver onError --> $t")
}

override fun onCompleted() {
println("responseObserver onNext --> onCompleted")
}
})

for (i in dataArr) {
val createAvgRequest1 =
CalcProto.FindMaximumRequest.newBuilder().setNumber(i).build()
call.onNext(createAvgRequest1)
delay(1000)
}

call.onCompleted()

} catch (e: Exception) {
e.printStackTrace()
}
}
}

Output received of Bidirectional Streaming RPC from server:

The output of Bidirectional Streaming RPC

Summary

A gRPC is an open-source framework that provides four RPC methods. With the help of RPC methods, we can communicate with the backend without using rest APIs, and in this article, we learned all the steps to integrate gRPC in Android and how to use all the RPC methods with examples.

I hope this helps! That’s all! Thank you for reading.

Happy Learning!

Special thanks to Akshata Shelar and Sarthak Kapoor for sharing ideas and valuable feedback for this article.

--

--