1

Anyone help me to understand how can I generate protobuf in Kotlin? I heard about gRPC, wire, KotlinPoet, but I don't understand what are the differences, which one should I use any samples, any simple documents please fill free to share with me? Can anyone provide a link for a sample which shows how to generate Protobuf sample to Kotlin?

ASLAN
  • 629
  • 1
  • 7
  • 20
Artur A
  • 257
  • 3
  • 20

1 Answers1

2

I've used both square/wire and io.grpc libraries to communicate with a gRPC service. The problem with the wire is that it does not support the proto3 version yet.

https://github.com/square/wire/issues/279

Here, I'll provide you an example of how to start with io.grpc.

Suppose that there is a gRPC service which sums up the stream of numbers you're sending to it. Its proto file is gonna be something like:

accumulator.proto

syntax = "proto3";

package accumulator;

service Accumulator {
    rpc NumberStream (stream NumberRequest) returns (stream AccumulateReply) {
    }
}

message NumberRequest {
    int32 number = 1;
}

message AccumulateReply {
    int64 sumUp = 1;
}

You should put this file under /src/main/proto/ directory of the project.

Now it's time to add the required dependencies to build.gradle file. Note that it uses kapt to generate codes.


App Level's build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.protobuf'
apply plugin: 'kotlin-kapt'

android {
    
    ... others

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8
    }
}

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' // CURRENT_GRPC_VERSION
        }
    }

    generateProtoTasks {
        all().each { task ->
            task.plugins {
                javalite {}
                grpc { // Options added to --grpc_out
                    option 'lite'
                }
            }
        }
    }
}

dependencies {
    ... other dependencies

    // ------- GRPC
    def grpc_version = '1.25.0'
    implementation "io.grpc:grpc-android:$grpc_version"
    implementation "io.grpc:grpc-okhttp:$grpc_version"
    implementation "io.grpc:grpc-protobuf-lite:$grpc_version"
    implementation "io.grpc:grpc-stub:$grpc_version"

    // ------- Annotation
    def javax_annotation_version = '1.3.2'
    implementation "javax.annotation:javax.annotation-api:$javax_annotation_version"
}

Project Level's build.gradle

buildscript {

    repositories {
        google()
        jcenter()
    }

    dependencies {
        ... others

        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
    }
}

Here is a class that encapsulates stream activities with the server. It returns received values through a callback:

AccumulatorHandler.kt

import android.content.Context
import io.grpc.ManagedChannel
import io.grpc.android.AndroidChannelBuilder
import io.grpc.stub.ClientCallStreamObserver
import io.grpc.stub.StreamObserver
import accumulator.AccumulatorOuterClass
import java.util.concurrent.Executors


/**
 * @author aminography
 */
class AccumulatorHandler constructor(
    private val context: Context,
    private val endPoint: String
) {

    var callback: AccumulatorCallback? = null

    private var managedChannel: ManagedChannel? = null

    private var requestObserver: StreamObserver<AccumulatorOuterClass.NumberRequest>? = null

    private val responseObserver: StreamObserver<AccumulatorOuterClass.AccumulateReply> =
        object : StreamObserver<AccumulatorOuterClass.AccumulateReply> {

            override fun onNext(value: AccumulatorOuterClass.AccumulateReply?) {
                callback?.onReceived(value.sumUp)
            }

            override fun onError(t: Throwable?) {
                callback?.onError(t)
            }

            override fun onCompleted() {
                callback?.onCompleted()
            }
        }

    fun offer(number: Int) {
        initChannelIfNeeded()
        requestObserver?.onNext(
            AccumulatorOuterClass.NumberRequest.newBuilder()
                .setNumber(number)
                .build()
        )
    }

    fun offeringFinished() {
        requestObserver?.onCompleted()
    }

    private fun initChannelIfNeeded() {
        if (managedChannel == null) {
            managedChannel = AndroidChannelBuilder.forTarget(endPoint)
                .context(context)
                .usePlaintext()
                .executor(Executors.newSingleThreadExecutor())
                .build()
        }
        if (requestObserver == null) {
            requestObserver = AccumulatorGrpc.newStub(managedChannel)
                .withExecutor(Executors.newSingleThreadExecutor())
                .numberStream(responseObserver)
        }
    }

    fun release() {
        (requestObserver as? ClientCallStreamObserver<*>)?.cancel("Cancelled by client.", null)
        requestObserver = null

        managedChannel?.shutdown()
        managedChannel = null

        callback = null
    }

    interface AccumulatorCallback {
        fun onReceived(sum: Long)
        fun onError(t: Throwable?)
        fun onCompleted()
    }

}

In order to test it, I've written an activity class to show its usage in a simple manner:

MyActivity.kt

/**
 * @author aminography
 */
class MyActivity: AppCompatActivity, AccumulatorHandler.AccumulatorCallback {

    private var accumulatorHandler: AccumulatorHandler? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        accumulatorHandler = AccumulatorHandler(applicationContext, "http://...")
        accumulatorHandler.callback = this

        for (number in 1..10){
            accumulatorHandler.offer(number)
        }
        accumulatorHandler.offeringFinished()
    }

    override fun onReceived(sum: Long) {
        Toast.makeText(this, "onReceived: $sum", Toast.LENGTH_SHORT).show()
    }

    override fun onError(t: Throwable?) {
        Toast.makeText(this, "onError: $t", Toast.LENGTH_SHORT).show()
        accumulatorHandler.release()
    }

    override fun onCompleted() {
        Toast.makeText(this, "onCompleted", Toast.LENGTH_SHORT).show()
        accumulatorHandler.release()
    }
}
aminography
  • 21,986
  • 13
  • 70
  • 74
  • Hey, thanks for answering, I don't see where are you adding dependencies to wire? – Artur A Jun 28 '20 at 09:36
  • 1
    And I see you are using in Kotlin but wondering the class that generated for accumulator.proto is it java or Kotlin? – Artur A Jun 28 '20 at 09:39
  • You're welcome. I mentioned in the answer, this is an implementation using `io.grpc`. – aminography Jun 28 '20 at 09:40
  • Both `wire` and `io.grpc` generate boilerplate classes in Java. – aminography Jun 28 '20 at 09:41
  • I see, but there is possible to implement in Kotlin as well,yes? – Artur A Jun 28 '20 at 19:50
  • We know that the kotlin and java are interoperable bidirectionally, but kotlin has more capabilities that when they come to java, the syntax gets a bit inconvenient, like extension functions, etc. So, these libraries generate codes in java and this is a choice of them, not us. Therefore we have no control over it to generate codes in kotlin. – aminography Jun 29 '20 at 06:46
  • 1
    Note: Since version 3.3.0, wire officially supports Proto 3! https://github.com/square/wire/blob/master/CHANGELOG.md#version-330 – shoheikawano Oct 31 '20 at 06:43
  • 1
    @shoheikawano: Great! Thanks for announcing here. – aminography Oct 31 '20 at 07:14
  • Wire supports Proto3 now. – oldergod Dec 09 '20 at 21:22