How can I generate protobuf in Kotlin for Android applications?

4k Views Asked by At

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?

1

There are 1 best solutions below

9
On

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()
    }
}