How do you add gRPC to Android Studio with Kotlin?

2k views Asked by At

Task

I need to connect an Android client with a python server using gRPC. Making the server and generating the protos was easy in Python, but the lack of tutorials and confusing documentation for the Kt client makes it appear overwhelmingly complicated.

Background

Until now I've made some simple Android apps using Kotlin, I got used to adding dependencies to either the module or app level build.gradle.

What have I tried?

My first thought was to go to the official documentation as I did with Python. I found the guide from there pretty confusing (I felt like there's something missing from that article), so I went to see the full examples from their GitHub. I also cloned the repo and compiled the protos with the gradlew installDist command. Then the things got awfully complicated:

  • When you create an Android Studio project, you get a bunch of gradle things(module and app level build.gradle's, gradlew and gradlew.bat, settings, etc)
  • After you clone the repo, you get another bunch of gradle things inside the grpc-kotlin folder.
  • You also get build.gradle.kts which seem to be the same build logic/package manager helper files, but with other dependencies and with the Kotlin Script syntax.

This is when I went off to YouTube in order to search for a simple implementation and found out that there's only a handful of videos on the gRPC with Kotlin subject, and most of those are presentation videos about the features of gRPC in Kotlin when using Coroutines.

What I have until now

I migrated all my build.gradle's to .kts ones. This is how my module-level build.gradle.kts looks like:

buildscript {
    val kotlin_version = "1.5.10"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:4.2.1")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}")
        classpath("org.jetbrains.kotlin:kotlin-android-extensions:${kotlin_version}")
        classpath("com.google.gms:google-services:4.3.8")
        classpath ("com.google.protobuf:protobuf-gradle-plugin:0.8.14")

    }
}

allprojects {
    repositories {
        google()
        mavenCentral()

    }
}

tasks.register("clean",Delete::class){
    delete(rootProject.buildDir)
}

This is how my app level build.gradle.kts looks like:

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 {
    id("com.android.application")
    id("com.google.protobuf")
    kotlin("android")
}

android {
    compileSdkVersion(30)
    buildToolsVersion = "30.0.3"

    defaultConfig {
        applicationId = "com.example.myapplication"
        minSdkVersion(26)
        targetSdkVersion(30)
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
        }
    }

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

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.12.0" }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.35.0"
        }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.plugins.create("java") {
                option("lite")
            }
            task.plugins {
                id("grpc") {
                    this.option("lite")
                }

            }
        }


    }
}

dependencies {

    val kotlin_version = "1.5.10"
    implementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}")
    implementation("androidx.core:core-ktx:1.5.0")
    implementation("androidx.appcompat:appcompat:1.3.0")
    implementation("com.google.android.material:material:1.3.0")
    implementation("androidx.constraintlayout:constraintlayout:2.0.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.2")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")

    // GRPC Deps
    implementation("io.grpc:grpc-okhttp:1.37.0")
    implementation("io.grpc:grpc-protobuf-lite:1.37.0")
    implementation("io.grpc:grpc-stub:1.36.0")
    implementation("org.apache.tomcat:annotations-api:6.0.53")
}

I could generate the protos but something was off about them.

Problem

When implementing the request functions, respectively a bi-directional stream, I found out that all my rpc functions asked for an extra StreamObserver parameter(which was absent in all of the tutorials I've found on the internet). At a closer look I observed that all the generated files were in java and on the official docs, the generated files are both POJOs and Kotlin.

This is how my generated Stub class looks like:

  public static final class ChatServiceStub extends io.grpc.stub.AbstractAsyncStub<ChatServiceStub> {
    private ChatServiceStub(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      super(channel, callOptions);
    }

    @java.lang.Override
    protected ChatServiceStub build(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      return new ChatServiceStub(channel, callOptions);
    }

    /**
     * <pre>
     * This bi-directional stream makes it possible to send and receive Notes between 2 persons
     * </pre>
     */
    public void chatStream(grpc.Chat.Empty request,
        io.grpc.stub.StreamObserver<grpc.Chat.Note> responseObserver) {
      io.grpc.stub.ClientCalls.asyncServerStreamingCall(
          getChannel().newCall(getChatStreamMethod(), getCallOptions()), request, responseObserver);
    }

    /**
     */
    public void sendNote(grpc.Chat.Note request,
        io.grpc.stub.StreamObserver<grpc.Chat.Empty> responseObserver) {
      io.grpc.stub.ClientCalls.asyncUnaryCall(
          getChannel().newCall(getSendNoteMethod(), getCallOptions()), request, responseObserver);
    }
  }

I do not know how to replicate a gradle script for my project, I found no one on the internet explaining how are all those build.gradle's linked together(I figured out that module level build.gradle's are describing how the module they're in is supposed to build and app level build.gradle's are idem but for the entire app). Most of the articles I found are the same as the official docs.

What I want

I just want a simple-simple project or a step by step tutorial, without "clone this and run a command in the terminal, it just works".

I do not blame the devs or whoever wrote the official docs, I actually bet I'm the stupid one here, but I struggle to understand these concepts and I would be grateful if someone can explain to me what I did wrong or where to learn.

Also, sorry for the long question, I tried to expose my POV the best I could, this is my second question since I started learning programming and I'm sorry if the problem and my goals aren't clear enough, I'll edit anything if it's needed.

3

There are 3 answers

0
Timothy Virgillo On

I do not have a step-by-step process I can share and do not want to trivialize an excellently asked question. However, I wanted to respond that in researching a similar problem, I found that Square has a library that seems to be more Kotlin friendly:

https://square.github.io/wire/#wire-kotlin

0
HSMKU On

Best solution is to read the official tutorial from grpc.io.

Here's also the official build.gradle.kts from the tutorial that make the compilation generate the proto stubs.

My protobuf :

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.23.4"
    }
    plugins {
        create("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.57.1"
        }
        create("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.0:jdk8@jar"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                create("grpc")
                create("grpckt")
            }
            it.builtins {
                create("java") //needed either it throws Unresolved Reference
                create("kotlin")
            }
        }
    }

}
6
Konrad Sikorski On

I created the simplest project I could. The minimal configuration of Android project can be found in this commit.

Start a new project from Basic activity template in Android Studio and then:

Modify app/build.gradle

  1. Add Gradle plugin, so it can generate all stubs in the build step:
plugins {  // this section should be already at the top of the file
    // ...
    id 'com.google.protobuf' version "0.9.1"  // "0.9.2 causes compilation errors!"
}

WARNING: At the time of writing, the latest version of protobuf gradle plugin is 0.9.2. Using this version causes build errors I wasn't able to deal with using information I found on the Internet.

  1. Add project dependencies. Lack of one can cause usually non-understandable error messages.
dependencies {  // this section should be already in the file
    // ...
    implementation 'io.grpc:grpc-stub:1.52.1'
    implementation 'io.grpc:grpc-protobuf:1.52.1'
    implementation 'io.grpc:grpc-okhttp:1.52.1'
    implementation 'io.grpc:protoc-gen-grpc-kotlin:1.3.0'
    implementation 'io.grpc:grpc-kotlin-stub:1.3.0'
    implementation 'com.google.protobuf:protobuf-kotlin:3.21.12'
}
  1. Add protobuf section - the one that is responsible for generating protobuf data classes and gRPC client stubs:
protobuf {  // this section needs to be added
    protoc {
        artifact = "com.google.protobuf:protoc:3.21.12"
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:1.52.1"
        }
        grpckt {
            // I don't really know what ":jdk8@jar" does...
            artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.0:jdk8@jar"
            // ...but it doesn't work without it.
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                grpc {}
                grpckt {}
            }
            it.builtins {
                kotlin {}
                java {}
            }
        }
    }
}

4*. I placed my proto file in app/src/main/proto directory. In case you store your protos in some other directory you can configure it by following instructions from protobuf-gradle-plugin repo readme.

Update AndroidManifest.xml

You also need to add proper permission to app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <!-- ... -->
</manifest>

Use gRPC client

class FirstFragment : Fragment() {
    // ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val port = 50051
        val channel = ManagedChannelBuilder.forAddress("192.168.0.13", port).usePlaintext().build()
        val stub = PingPongGrpcKt.PingPongCoroutineStub(channel)

        binding.buttonFirst.setOnClickListener {
            runBlocking {
                val request = Pingpong.PingPongMsg.newBuilder().setPayload("World").build()
                val response = stub.ping(request)
                Log.i("result", response.toString())
            }
        }
    }
    // ...
}