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.
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
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
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.
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'
}
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())
}
}
}
// ...
}
Related
I have a kotlin multiplatofrm library that is included into an Android and iOS app.
In my android project include it as a composite build (MyLib). But Intellisense is not working at all for all code from in MyLib, though the whole thing compiles fine. I am using Android Studio. What could be wrong and how can I debug it?
rootProject.name='xxx'
includeBuild 'MyLib'
include ':common'
include ':app'
MyLib's build.gradle.kts looks as follows:
plugins {
kotlin("multiplatform") version "1.5.31"
kotlin("native.cocoapods") version "1.5.31"
}
repositories {
mavenCentral()
maven { setUrl("https://dl.bintray.com/kotlin/kotlinx.html/") }
}
group = "com.xxx.MyLib"
// CocoaPods requires the podspec to have a version.
version = "1.0"
kotlin {
ios()
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnit()
}
}
cocoapods {
ios.deploymentTarget = "11.4"
frameworkName = "MyLib"
summary = "xxx"
homepage = "xxx"
podfile = project.file("../../iOS-App/Podfile")
}
sourceSets {
commonMain {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.31")
implementation("com.badoo.reaktive:reaktive:1.2.0")
implementation("com.badoo.reaktive:reaktive-annotations:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1")
implementation("com.russhwolf:multiplatform-settings-no-arg:0.8.1")
implementation("net.swiftzer.semver:semver:1.1.1")
}
}
}
}
tasks.withType<GenerateModuleMetadata> {
enabled = true
}
I think this is likely related to https://youtrack.jetbrains.com/issue/KTIJ-18903
I had the same issue and it drove me crazy. How can one write code nowadays without Intellisense (answer: you can't).
I tried a ton of things (all the usual and unusual stuff you do when Android Studio / IntelliJ act up). Ultimately I upgraded to Kotlin 1.6.0-RC2 (from 1.5.31) -> https://github.com/JetBrains/kotlin/releases/tag/v1.6.0-RC2.
Part of that is upgrading the Kotlin Plugin:
Another part is the Kotlin Gradle Plugin dependency:
org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0-RC2
And last but not least I had to downgrade the corouting dependency (from 1.5.2):
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-RC
After that everything was back to normal.
I'm trying to develop a gradle plugin to use it for generating some objects and methods for our api using some scheme.
I have followed some tutorials but they all seem not to work, atleast for me.
Some of these tutorials were:
https://musings.animus.design/kotlin-poet-building-a-gradle-plugin/
https://medium.com/#magicbluepenguin/how-to-create-your-first-custom-gradle-plugin-efc1333d4419
I have not used the buildSrc module because I'm already using it for Kotlin DSL, so I decided to create a new module and create my plugin there.
My plugin's module build.gradle.kts looks like this:
plugins {
id("java-gradle-plugin")
id("kotlin")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath(config.ClassPaths.androidBuildTools)
classpath(config.ClassPaths.kotlinGradlePlugin)
}
}
repositories {
google()
jcenter()
}
dependencies {
implementation(config.ClassPaths.androidBuildTools)
implementation(config.ClassPaths.kotlinGradlePlugin)
}
gradlePlugin {
plugins {
create("Generator") {
id = "Generator"
implementationClass = "Generator"
}
}
}
My projects settings.gradle.kts looks like this:
include(":SampleProject", ":scheme-generator")
And in my application module's build.gradle.kts I'm applying this plugin like this:
apply(plugin = "Generator")
The build script stops here with an error: plugin 'Generator' not found
My Generator class looks like this:
class Generator : Plugin<Project> {
override fun apply(target: Project) {
target.android().variants().all { variant ->
// Make a task for each combination of build type and product flavor
val myTask = "myFirstTask${variant.name.capitalize()}"
// Register a simple task as a lambda. We can later move this to its own
// class to make our code cleaner and also add some niceties.
target.tasks.create(myTask){task ->
// Group all our plugin's tasks together
task.group = "MyPluginTasks"
task.doLast {
File("${target.projectDir.path}/myFirstGeneratedFile.txt").apply {
writeText("Hello Gradle!\nPrinted at: ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())}")
}
}
}
}
}
}
The android and variants methods are declared in a utils file, they look like this:
object GeneratorUtils {
fun Project.android(): BaseExtension {
val android = project.extensions.findByType(BaseExtension::class.java)
if (android != null) {
return android
} else {
throw GradleException("Project $name is not an Android project")
}
}
fun BaseExtension.variants(): DomainObjectSet<out BaseVariant> {
return when (this) {
is AppExtension -> {
applicationVariants
}
is LibraryExtension -> {
libraryVariants
}
else -> throw GradleException("Unsupported BaseExtension type!")
}
}
}
I have tried many things, but I seem not to get this right.
EDIT:
Using the buildSrc module for my plugin works totally fine, the plugin is applied and the gradle tasks are visible. However, buildSrc is reserved for other purposes, and we would like our plugin to be in a separate module, so we will be able to use it in other projects.
EDIT 13/04/2021
I have managed to see the tasks that are added by my plugin in my app tasks list by including this module as a composite build.
My settings.gradle now looks like this:
pluginManagement {
includeBuild("generator")
}
include(":SampleProject")
build.gradle of my plugin looks like this:
apply plugin: 'java-gradle-plugin' // Allows us to create and configure custom plugins
apply plugin: 'kotlin' //Needed as we'll write our plugin in Kotlin
buildscript {
ext {
kotlin_version = '1.4.31'
gradle_version = '4.1.2'
}
repositories {
google()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.android.tools.build:gradle:$gradle_version"
}
}
repositories {
google()
jcenter()
}
dependencies {
// Android gradle plugin will allow us to access Android specific features
}
gradlePlugin {
plugins {
create("Generator") {
id = "Generator"
implementationClass = "com.example.Generator"
}
}
}
And my generator class now looks like this:
class Generator : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register("generationTask") { task ->
task.apply {
group = "generation"
actions.add(Action {
print("Hello from generation task!")
})
}
}
}
}
I can see generationTask in my tasks list and I can execute it normally. It prints the text without any problems.
The problem now is to include com.android.tools.build:gradle:4.1.2 in my dependecies to use it to access build types and flavors and their paths to save my generated code there. When I add it to my dependencies block, gradle fails with this error: Could not find com.android.tools.build:gradle:4.1.2.
How can I solve this problem?
Gradle build goes through specific set of phases, and the Configuration phase comes before the Execution phase. So you cannot use a plugin, which is built in the same build process, because by the time gradle tries to use it on Configuration phase, the plugin has not been built yet.
buildSrc directory is a special one, it's built not as part of the same build process, but in a separate build, before the main build process starts. This feature is called included or composite build. buildSrc is just a pre-defined way to set up a composite build and you can define your own included builds. So to make your plugin visible to the main build, you need to put it into a separate build and include this build into a composite build as described in the doc above. Here is an article describing how to transform a plugin defined in buildSrc into a composite build.
I have created a Kotlin multiplatform project using Intelli-j IDE (community edition) following a tutorial from this site:
https://medium.com/#cafonsomota/set-up-your-first-kotlin-multiplatform-project-for-android-and-ios-april-2020-258e2b1d9ef4
What I have not followed is the xCode part of the tutorial since, at this point although I want this project to be multiplatform, my primary interest is for Android.
When I am running the common sample tests, I see the error:
e: org.jetbrains.kotlin.konan.MissingXcodeException: An error occurred during an xcrun execution. Make sure that Xcode and its command line tools are properly installed.
I can also see that the for the associated configuration, tasks details the following:
cleanIosTest iosTest
That is why I am receiving the error.
What I can't figure out is how to change Sample Test to not run that configuration. I have tried to remove those tasks, Apply and Save but they keep reappearing when I run them. I can't see anything in the build.gradle files that indicates anything iOS specific for testing.
build.gradle.app
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.72'
}
repositories {
google()
jcenter()
mavenCentral()
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId 'org.jetbrains.kotlin.mpp_app_android'
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName '1.0'
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
buildTypes {
release {
minifyEnabled false
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
}
kotlin {
android("android")
// This is for iPhone emulator
// Switch here to iosArm64 (or iosArm32) to build library for iPhone device
iosX64("ios") {
binaries {
framework()
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
androidMain {
dependencies {
implementation kotlin('stdlib')
}
}
androidTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
iosMain {
}
iosTest {
}
}
}
// This task attaches native framework built from ios module to Xcode project
// (see iosApp directory). Don't run this task directly,
// Xcode runs this task itself during its build process.
// Before opening the project from iosApp directory in Xcode,
// make sure all Gradle infrastructure exists (gradle.wrapper, gradlew).
task copyFramework {
def buildType = project.findProperty('kotlin.build.type') ?: 'DEBUG'
def target = project.findProperty('kotlin.target') ?: 'ios'
dependsOn kotlin.targets."$target".binaries.getFramework(buildType).linkTask
doLast {
def srcFile = kotlin.targets."$target".binaries.getFramework(buildType).outputFile
def targetDir = getProperty('configuration.build.dir')
copy {
from srcFile.parent
into targetDir
include 'app.framework/**'
include 'app.framework.dSYM'
}
}
}
The Sample tests look like this:
package sample
import kotlin.test.Test
import kotlin.test.assertTrue
class SampleTests {
#Test
fun testMe() {
assertTrue(Sample().checkMe() > 0)
}
#Test
fun testProxy() {
assertTrue(Proxy().proxyHello().isNotEmpty())
}
}
The configuration looks like this:
Does anyone know how I can resolve this without needing to download xCode? I am happy to share any other information but am not truthfully sure what I should be sharing for this.
Incidentally, I did create another configuration without that line but, when I press the green PLAY button on the first test, it always defaults to the Sample Test configuration with the iOS tasks in.
This problem is caused by the bug, already described in the Kotlin issue tracker, see here. If you want to run Android-only tests, you should go with Gradle tasks named test<Debug | Release>UnitTest instead of the allTests, which includes the only iOS in your case.
I'm trying to add logging for Ktor http requests in Android application. According to docs I have to add gradle dependency
implementation "io.ktor:ktor-client-logging:$ktor_version"
and just use this snippet
val client = HttpClient() {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
}
Problem is that compiler "ignores" package 'io.ktor.client.features.logging' added as a dependency. What's strange is that JsonFeature (added as similar dependency) works just fine.
install(JsonFeature) { // perfectly works
...
}
install(Logging) { // unresolved reference
...
}
I already checked .jar file that gradle added to the project, it contains all expected classes, I can open them and see the source code, but magically just can't use in my app. After hours of research I guess it may be somehow related to gradle metadata or that logging feature is multiplatform and some additional gradle configuration is required, but unfortunately I'm not a gradle expert.
I tried adding enableFeaturePreview("GRADLE_METADATA") to settings.gradle, but no effect. Even tried to add "-jvm" to dependency.
implementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
With this dependency Android Studio finding package successfully, but fails to compile with following error
More than one file was found with OS independent path 'META-INF/ktor-http.kotlin_module'
Can anyone please clarify how to properly configure dependency for Ktor logger?
For the ktor-client-logging you have to have the dependency set for each platform:
commonMain {
dependencies {
implementation "ch.qos.logback:logback-classic:1.2.3"
implementation "io.ktor:ktor-client-logging:$ktor_version"
}
}
androidMain {
dependencies {
implementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
}
}
iosMain {
dependencies {
implementation "io.ktor:ktor-client-logging-native:$ktor_version"
}
}
as for the meta META-INF/ktor-http.kotlin_module add to the app/build.gradle inside the android {} block:
android {
packagingOptions {
exclude 'META-INF/common.kotlin_module'
exclude 'META-INF/*.kotlin_module'
}
}
I'm trying to use dokka on my android project to generate kdoc.
But I have this error when I'm running the script 'modules:app [dokka]' :
Could not determine the dependencies of task ':app:dokka'.
kotlin.KotlinNullPointerException (no error message)
I added the following lines on my gradle files :
Project build.gradle
buildscript {
ext {
dokka_version = '0.9.18'
}
dependencies {
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:$dokka_version"
}
}
app build.gradle
plugins {
id 'org.jetbrains.dokka-android'
}
dokka {
outputFormat = 'html'
sourceDirs = files('src/main')
outputDirectory = "$buildDir/javadoc"
}
Could not determine the dependencies of task ':app:dokka'.
kotlin.KotlinNullPointerException (no error message)
The issue is that it's a multiplatform project. In the app level gradle file, I'm also applying the org.jetbrains.kotlin.multiplatform plugin. As described in the dokka github release page:
Experimental Kotlin Multiplatform support is scheduled for 0.9.19
Looks like there's no other solution than wait for the next release of dokka.
Edit: There's a workaround described on the kolinlang forum
dokka {
impliedPlatforms = ["common"] // This will force platform tags for all non-common sources e.g. "JVM"
kotlinTasks {
// dokka fails to retrieve sources from MPP-tasks so they must be set empty to avoid exception
// use sourceRoot instead (see below)
[]
}
sourceRoot {
// assuming there is only a single source dir...
path = kotlin.sourceSets.commonMain.kotlin.srcDirs[0]
platforms = ["common"]
}
}