Build Microservices With Kotlin and gRPC Server | by Wynne Tran | Aug, 2022

Part 1 — gRPC server and Android Client in Kotlin

In case you have never learned of microservices in Kotlin and Grpc before, I recommend you to do a web search for “Grpc” and “Kotlin”, then look over some documents that can help you understand them first. Subsequently, this article will show step-by-step how to build microservices with Kotlin and gRPC.

I’ll divide this article into 2 parts:

Part 1:

  • Build 1 gRPC server
  • Build Android client

Part 2:

  • Build multi gRPC servers in the same project
  • Build iOS client

Now, let’s start.

I will use Android Studio and install Kotlin Multiplatform Mobile for this project.

After installing the plugin, create a new project and choice “Kotlin Multiplatform App” -> Next -> name the project as KotlinGrpc

Note: If you don’t give the same package name dandelion.net.kotlingrpcevery time you copy and paste my code, please take care of your package name because it is easy to get the error.

The KotlinGrpc project structure will look like the image below.

  1. Building gRPC server

a. Configuration build.gradle project.

Go to KotlinGrpc/build.gradle.kts (build.gradle project)

plugins {
id("com.google.protobuf") version "0.8.15" apply false
kotlin("jvm") version "1.5.31" apply false
id("com.android.application") version "7.2.1" apply false
id("org.jetbrains.kotlin.android") version "1.5.31" apply false
}

ext["grpcVersion"] = "1.46.0"
ext["grpcKotlinVersion"] = "1.3.0" // CURRENT_GRPC_KOTLIN_VERSION
ext["protobufVersion"] = "3.20.1"

allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
}
}

NOTE: You have to click “Sync Now” (notification on the top right side) after you edit something in gradle.build.kts

b. Adding new module PROTOS

Right click root folder (KotlinGrpc folder) -> New -> Module

Choice “Java or Kotlin Library” -> name library as protos

You can delete some redundant files like MyClass

Go to protos/build.gradle.ktsadding configuration then click “Sync Now”

plugins {
`java-library`
}
java {
sourceSets.getByName("main").resources.srcDir("src/main/java")
}

Next, go to protos/src/main/java/dandelion.net.protosadd a new file and name it hello_service.proto

We will get a sample HelloWorldService from the gRPC document, it includes 1 request from the client and 1 response from the server.

This is a definition of a proto file:

Serialized data is defined in configuration files called proto files (. proto). Configurations known as messages will be stored in these files. Compiling the proto files generates code in the user’s programming language.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "dandelion.net.protos";
option java_outer_classname = "HelloWorldService";

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
bool received = 2;
}

c. Adding new module STUB

Right click root folder (KotlinGrpc folder) -> New -> Module -> choice “Java or Kotlin Library” -> name library stub

This step is a power of gRPC, when we run the project, google will generate code for us and store them into the stub module. Let’s go to:

stub/build.gradle.kts:

Configure API dependencies and set the sourceSets (where are the files generated automatically)

import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc

plugins {
kotlin("jvm")
id("com.google.protobuf")
}

dependencies {
protobuf(project(":protos"))

api(kotlin("stdlib"))
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")

api("io.grpc:grpc-protobuf:${rootProject.ext["grpcVersion"]}")
api("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}")
api("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}")
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
}

sourceSets {
val main by getting { }
main.java.srcDirs("build/generated/source/proto/main/grpc")
main.java.srcDirs("build/generated/source/proto/main/grpckt")
main.java.srcDirs("build/generated/source/proto/main/java")
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}"
}
plugins {
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcVersion"]}"
}
id("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:0.2.0:jdk7@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
id("grpc")
id("grpckt")
}
}
}
}

Now, look at the “stub” module when we still have not built the project yet.

Then, click build project or click the “hammer” icon on the top to build the app, after that we will have the file we want.

d. Adding new module SERVER

Right click root folder (KotlinGrpc folder) -> New -> Module -> choice “Java or Kotlin Library” -> name library Server

Like these steps above, we are going to add configuration to build.gradle.kts

Go to server/build.gradle.kts:

plugins {
application
kotlin("jvm")
}

dependencies {
implementation(project(":stub"))
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.5.2")
runtimeOnly("io.grpc:grpc-netty:${rootProject.ext["grpcVersion"]}")
}

tasks.register<JavaExec>("HelloServer") {
dependsOn("classes")
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("dandelion.net.server.HelloServerKt")
}

val helloServerStartScripts = tasks.register<CreateStartScripts>("helloServerStartScripts") {
mainClass.set("dandelion.net.server.HelloServerKt")
applicationName = "hello-server"
outputDir = tasks.named<CreateStartScripts>("startScripts").get().outputDir
classpath = tasks.named<CreateStartScripts>("startScripts").get().classpath
}

tasks.named("startScripts") {
dependsOn(helloServerStartScripts)
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
}

Next, go to server/src/main/java/dandelion/net/server, add a new Kotlin file and name it HelloServer.kt. Note that you can delete some redundant files like MyClass

package dandelion.net.server

import dandelion.net.protos.GreeterGrpcKt
import dandelion.net.protos.HelloReply
import dandelion.net.protos.HelloRequest
import io.grpc.Server
import io.grpc.ServerBuilder
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors

class HelloServer(private val port: Int) {
val server: Server = ServerBuilder
.forPort(port)
.addService(HelloService())
.build()

fun start() {
server.start()
println("Server started, listening on $port")
Runtime.getRuntime().addShutdownHook(
Thread {
this@HelloServer.stop()
println("*** server shut down")
}
)
}

private fun stop() {
server.shutdown()
}
fun blockUntilShutdown() {
server.awaitTermination()
}

private class HelloService : GreeterGrpcKt.GreeterCoroutineImplBase(
coroutineContext = Executors.newFixedThreadPool(
1
).asCoroutineDispatcher()) {
override suspend fun sayHello(request: HelloRequest) : HelloReply {
return HelloReply.newBuilder()
.setMessage("Hello " + request.name)
.build()
}
}

}

fun main() {
val port = System.getenv("PORT")?.toInt() ?: 8080
val server = HelloServer(port)
server.start()
server.blockUntilShutdown()
}

Explain:

  • class HelloServer(private val port: Int) — > this class build the port server.
  • private class HelloService : GreeterGrpcKt.GreeterCoroutineImplBase(…) — > this class use Kotlin-Coroutine to call the function we create in the proto file. GreeterGrpcKt is a class that google generates inside the stub module for us.

Now, click the “run button” to run the server, we will see the server start at port 8080.

2. Building Android Client

Let’s add dependencies to androidApp/build.gradle.kts file, we will define the IP address that we want to connect from the server here (serverUrl)

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

android {

sourceSets["main"].java.srcDir("src/main/java")

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
compose = true
}
kotlinOptions {
jvmTarget = "1.8"
}

composeOptions {
kotlinCompilerExtensionVersion = composeVersion
}

packagingOptions {
resources.excludes += "META-INF/kotlinx_coroutines_core.version"
}

compileSdk = 32

defaultConfig {
applicationId = "dandelion.mobile.android"
minSdk = 21
targetSdk = 32
versionCode = 1
versionName = "1.0"

val serverUrl: String? by project
if (serverUrl != null) {
resValue("string", "server_url", serverUrl!!)
} else {
resValue("string", "server_url", "http://10.0.2.2:8080/")
}

testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"

testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}

val composeVersion = "1.1.0"

dependencies {

implementation(project(":stub"))
implementation(kotlin("stdlib"))
implementation("androidx.activity:activity-compose:1.5.1")
implementation("androidx.appcompat:appcompat:1.4.2")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2")
implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
implementation("androidx.compose.material:material:$composeVersion")
implementation("androidx.compose.runtime:runtime:$composeVersion")
implementation("androidx.compose.ui:ui:$composeVersion")
runtimeOnly("io.grpc:grpc-okhttp:${rootProject.ext["grpcVersion"]}")

}

Next, we begin to build UI with @Composableso we do not need layoutand delete the layout folder inside androidApp/src/main/res

Go to androidApp/src/main/res/values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="name_hint">Name:</string>
<string name="send_request">Send gRPC Request</string>
<string name="app_label">gRPC Kotlin Android</string>
<string name="server_response">Server response: </string>
</resources>

Go to androidApp/src/main/AndroidManifest.xml and add INTERNET permission

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="dandelion.net.kotlingrpc.android">

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

<application
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@android:drawable/btn_star"
android:label="@string/app_label">
<activity
android:theme="@style/Theme.AppCompat.NoActionBar"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>

Finally, Go to androidApp/src/main/java/dandelion/net/kotlingrpc/android/MainActivity.kt:

package dandelion.net.kotlingrpc.android
import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import dandelion.net.kotlingrpc.android.R
import dandelion.net.protos.GreeterGrpcKt
import dandelion.net.protos.HelloRequest
import io.grpc.ManagedChannelBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.launch
import java.io.Closeable

class MainActivity : AppCompatActivity() {
private val uri by lazy { Uri.parse((resources.getString(R.string.server_url))) }
private val greeterService by lazy { GreeterRCP(uri) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(color = MaterialTheme.colors.background) {
Greeter(greeterService)
}
}
}
override fun onDestroy() {
super.onDestroy()
greeterService.close()
}
}

class GreeterRCP(uri: Uri) : Closeable {
val responseState = mutableStateOf("")

private val channel = let {
println("Connecting to ${uri.host}:${uri.port}")
val builder = ManagedChannelBuilder.forAddress(uri.host, uri.port)
if (uri.scheme == "https") {
builder.useTransportSecurity()
} else {
builder.usePlaintext()
}
builder.executor(Dispatchers.IO.asExecutor()).build()
}

private val greeter = GreeterGrpcKt.GreeterCoroutineStub(channel)
suspend fun sayHello(name: String) {
try {
val request = HelloRequest.newBuilder().setName(name).build()
val response = greeter.sayHello(request)
responseState.value = response.message
} catch (e: Exception) {
responseState.value = e.message ?: "Unknown Error"
e.printStackTrace()
}
}
override fun close() {
channel.shutdownNow()
}
}

@Composable
fun Greeter(greeterRCP: GreeterRCP) {
val scope = rememberCoroutineScope()
val nameState = remember { mutableStateOf(TextFieldValue()) }
Column(Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally) {
Text(stringResource(R.string.name_hint), modifier = Modifier.padding(top = 10.dp))
OutlinedTextField(nameState.value, { nameState.value = it })
Button({ scope.launch { greeterRCP.sayHello(nameState.value.text) } }, Modifier.padding(10.dp)) {
Text(stringResource(R.string.send_request))
}
if (greeterRCP.responseState.value.isNotEmpty()) {
Text(stringResource(R.string.server_response), modifier = Modifier.padding(top = 10.dp))
Text(greeterRCP.responseState.value)
}
}
}

Explain:

  • class GreeterRCP(uri: Uri): Closeable — > this class is build a channel to connect with the server IP address, and call GreeterGrpcKt class inside the stub module.
  • fun Greeter(greeterRCP: GreeterRCP) — > this function for building @Composable UI for android app.

Now, enjoy what we have done.

Run HelloServer first, then run the Android client.

In case you miss something in my code, this GitHub repository is for you:

Some useful sources for you:

Leave a Comment