Share a Kotlin module with an Android and Desktop project - android

I have a game that I am working on that uses the LibGDX game framework. Currently the platforms I am targeting are Desktop (PC, Mac, Linux) via a platform independent jar and Android.
The project is hosted at https://github.com/NoxHarmonium/project-whiplash Feel free to take a look if you need to.
The bulk of the code is in a module called core and is written entirely in Kotlin. This module is linked into both the Desktop and Android projects.
This works fine for Android versions 7.1+ and for desktop. For all other versions of Android I get a pile of java.lang.NoClassDefFoundError exceptions on anonymous functions such as this:
val objectObservable = this.observableCache.computeIfAbsent(assetRef, fun(assetRef: AssetRef): Observable<T> {
return Async.start(fun(): T {
...
}).observeOn(this.eventLoopScheduler)
})
Exception Sample:
java.lang.NoClassDefFoundError: com.projectwhiplash.utils.assets.LibGdxDataManager$objectMapFromYaml$objectMapObservable$1
It seems to be caused by an incompatibility with the JVM that Kotlin targets by default (1.8) and the JVM level that older versions of Android support (1.6). I could be wrong but this explains why the latest version of Android works as it supports a later version of the JVM.
The solution should be as simple as forcing Kotlin to emit JVM byte code as version 1.6 but I can't seem to work it out. If you compile Kotlin directly into an Android, this seems to be handled by using the kotlin-android Gradle plugin. Unfortunately I can't use this plugin for the core module because it should not have any Android dependencies.
I tried to override the JVM version using the build settings mentioned at https://kotlinlang.org/docs/reference/using-gradle.html#compiler-options like this:
compileKotlin {
kotlinOptions {
jvmTarget = "1.6"
}
}
However, it did not seem to work no matter which Gradle file I placed it in. In fact I get a "Cannot resolve symbol 'kotlinOptions'" error shown by Intellij when I try it. It is possible that the Kotlin team have changed something and the documentation has not been updated.
I can override the Kotlin settings manually in the Intellij module settings but it gets overridden every time I sync the gradle project and is not a good long term solution. The project is designed to be IDE independent.
Does anyone know how I could set up the core module for max compatibility with older versions of Android?
I currently have the minimum API level set to 9 as this is the current LibGDX default but I'm willing to set this higher if it would be too difficult to target such a low API level.
Edit 1:
I just extracted the jar file produced by the core module and examined the class files using the javap tool.
I ran the following command on a random class file
java -verbose foo.class
and it output text with the following text
...
minor version: 0
major version: 50
...
using this question List of Java class file format major version numbers? I determined that the class file is actually targeting JVM 1.6.
Therefore my original theory is wrong and there is another reason why older Android versions cannot load classes generated by Kotlin lambdas.

It looks like you are using functionality that only exists within the JDK 8 library. Specifically the computeIfAbsent() method on the Map class.
Because of this, even though your code has been compiled down to JVM 1.6 compatibility, the underlying implementation on Android devices is missing that functionality and thus the reason for the NoClassDefFoundError exception you were seeing.
Updated: You can see in the javadoc located at https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfAbsent-K-java.util.function.Function- that the computeIfAbsent() has only been around since JDK 8

Related

Why I got LocalTime error on Android/Kotlin but I didn't plain Kotlin?

I've tried this library on plain Kotlin, it's working well. But when I've tried on Android, it doesn't work anymore. Why? What is the difference between codes that I wrote on Kotlin and Android/Kotlin? What is the connection between an API and Java library?
Error on Logcat:
java.lang.NoClassDefFoundError: Failed resolution of:
Ljava/time/LocalTime;
The line I got error is:
val current = LocalTime.now().hour
I've tried this library on plain Kotlin, it's working well. But when
I've tried on Android, it doesn't work anymore. Why?
Java 8 introduced new Date and time classes (and a lot of other utiility classes), previously developers mostly used Joda Time a third party library as the built-in java date classes where not reliable and limited in functionality. By default the java and kotlin compilers targets java 6 which is max supported upto android 25 (Nougat).
What is the connection between an API and Java library?
An api is built into the target device and alibrary has to be shipped with your app.
What is the difference between codes that I wrote on Kotlin and
Android/Kotlin
There is not much difference between the java and kotlin compiler, only the target jvm's. Though the kotlin compiler will usually compile code that will fail to run later as it heavily relies on annotations (which you setup)
Depending on your environment (target 6 or 8 for kotlin and java) and your devices (android version 25 and below or above 26) the code will have different classes available to it and setting the target jvm will allow consistancy across android versions.

How can I see dependency updates in build.gradle when using variables?

Android studio will show you if there is a newer version of a library when the version number is hard coded. But if you use variables, this seems to short circuit that functionality. Is there any way to get it back? I want to see available updates in build.gradle without having to Inspect the project.
Gradle 6 offers a new way to recommend and share versions between projects called platforms. This allows you to specify a set of dependency versions to share across projects without using variables.
You can use the Java Platform Plug-in to create a platform; a special kind of project that only contains a list of dependencies that work together. I haven't tried this, but hopefully Android studio will alert you if any of these versions are out of date since you'll be using hard coded version numbers.
You can then reference your platform in your other projects using the platform keyword and load all the other dependencies without versions
dependencies {
// get recommended versions from the platform project
api(platform(project(":platform")))
// no version required
api("commons-httpclient:commons-httpclient")
}

Retrolambda/Multimodule confusion

I am using retrolambda and some tests got me quite confused.
My understanding is:
with retrolambda I can use some java8 language features (e.g. lambdas), but not java 8 classes: e.g. without any additional libraries, I cannot use the java8 streams: HashSet<>.stream() is not available
I write my code with java-8 features (lambdas). The source code is translated into java8-class files and then retrolambda kicks in and converts the java8-class files to java7-class files which can be executed on older Android version (e.g. Android 6)
So, I've built a small test-project to test my assumptions and there are some things I am not sure of. I hope someone can clarify.
Heres a tag of my source code, that I will refer to.
in a multimodule android application (one android module and a java-only module), we must apply the retrolambda gradle plugin to all modules (app and java) that want to use java8 features
first the java module will be compiled by javac to java8-class files, retrolambda will convert them to java7 and a jar is built
then the app module will be compiled the same way as the java module: but now we get the apk instead of a jar: everything is java7 and can happily be used on Android 6
In my source code I have some unit tests, that deliberately use java8 code that should not work: I call HashSet<>.stream()
Android studio and the Emulator: All tests work
I guess that Android Studio & Emulators do not even use retrolambda at all and just compile and run everything to java8
Is there a way to change that?: e.g. it would be good if any of the tests already catch these problems
Gradle command line build:
gradle clean app:test: The unit tests work
gradle clean java:test: The unit tests fail
I have no idea why the tests in the app module work and those in the java module fail: why is that?
On a real device with Android 6 everything crashes, as expected

Kotlin REPL with Android Classes unStub!ed

I like the Kotlin REPL in Idea / Android-Studio - but as an Android Developer I often run into Stub! problems here. When writing unit-tests I am using unmock to work around this problem. Is there a way to use the same method used there for the Kotlin REPL plugin?
[
All android (and java.lang.*) classes are placeholders in an Android project. This is because android does not use standard java class files to store the compiled code and there is no way to directly run this code on a computer.
You simply can't use the REPL with android classes, they will only exist on an actual device or emulator.
If you do not care about correctness, then you can use Robolectric's implementation of Android by adding it as a dependency to the project.
To make sure it does not collide with the actual implementation you should probably do this with a separate module dedicated to the REPL.
Robolectic's dependency used by unmock is: org.robolectric:android-all:7.1.0_r7-robolectric-0
The problem is that the Kotlin REPL in IDEA is provided by the Kotlin IDEA plugin, which has no notion of Android per se, but only looks at what's in the classpath, which in this case is the android.jar containing the stubs that throw the exception you mentioned.
Using unmock or even the integrated Android support for removing exceptions from the stubs in tests (see here at the end of "Mock Android dependencies") won't work as that only affects your Gradle build.
The only solution I can think of is to either open an issue on the Kotlin tracker or dig through the source code of the REPL function in the Kotlin plugin and send a Pull Request.

Kotlin library 'classes.jar' has an unsupported format. Please update the library or the plugin

This message appears on project sync.
I've tried to clean and rebuild the project, but no success.
I'm using latest plugin version 0.12.275, "org.jetbrains.kotlin:kotlin-gradle-plugin:0.12.213" and "org.jetbrains.kotlin:kotlin-stdlib:0.12.213"
I've tried with the stable version 0.12.200 for both plugin and library, but I get the same error.
I'm using Android Studio AI-141.1972460 (canary channel).
Looks like the problem was in my *.aar lib, that was included in the project - it was compiled with an old version of Kotlin. I've upgraded the libary to the latest Kotlin version and it works now.
This issue was resolved with the updated library as mentioned by #ookami.kb
About the error message...
The "unsupported format" error comes when the ABI version number of the class files created by Kotlin does not match the expected used by the Kotlin compiler. This is no longer an issue with Kotlin 1.0 Betas since the ABI number will not change again for 1.0. But, there will be one forced recompile at 1.0 release candidate to ensure no old compiler bugs affect libraries or code and everything is rebuilt clean. Afterwards no issues such as this will exist.
Therefore if a library is not up to date with the same ABI, or hits this last "1.0 recompile" you may run into a similar error. The solution is always to find the updated library.
More about this in the Kotlin 1.0 Beta 4 announcement "What's Next" section:
After the Beta period is over, there will an RC and then 1.0.
We would really like to make sure that no code compiled with
pre-release versions of Kotlin are kept around after 1.0, so the RC
compiler will force recompilation of all the old code. We will
coordinate with library maintainers outside JetBrains to make sure
that all the widely-used libraries will be recompiled in time.
We’ll also take the opportunity to remove some legacy at this point:
remove all the deprecations that we have accumulated in the process of evolving our libraries,
remove all the deprecations from the generated code (you might not have heard of those, but they exist!),
get rid of some legacy bytecode peculiarities that were found during the beta,
move some of the stdlib code around so that the packages there have
more structure.
After that point, the only compatible changes to the
standard library are deprecations and additions (this does not include
reflection APIs). We are running an open review for the library API to
make sure we haven’t missed anything important.
This is Kotlin bug with new plugin version

Categories

Resources