Ivan Shafran
Posted on February 19, 2023
Kotlin versions support
When we develop a regular app for Android we can choose any Kotlin version. Some teams prefer to use the latest version in order to try new features and tooling improvements. Some teams wait some time before switching to a new version. It can be linked to a release cycle or waiting for community feedback on the last version.
But when we develop a library on Kotlin we have to support both versions. Luckily Kotlin has good forward and backward compatibility.
Backward compatibility
It’s simple:
All binaries are backwards compatible, i.e. a newer compiler can read older binaries (e.g. 1.3 understands 1.0 through 1.2) [1]
But it only applies to fully stable APIs: std-lib, coroutines. Check [2] for the full list.
Forward compatibility
There is such statement:
Preferably (but we can’t guarantee it), the binary format is mostly forwards compatible with the next feature release, but not later ones (in the cases when new features are not used, e.g. 1.3 can understand most binaries from 1.4, but not 1.5). [1]
Not very specific but in practice, my library which targets 1.3 works fine with 1.2 and even 1.1 std-lib without any changes. And it’s not working with 1.0 due to function reference feature which I used in the library code.
But if we want more strict guarantees Kotlin compiler provides two useful flags: apiVersion and languageVersion.[3]
-language-version X.Y
- compatibility mode for Kotlin language version X.Y, reports errors for all language features that came out later.[4]
-api-version X.Y
- compatibility mode for Kotlin API version X.Y, reports errors for all code using newer APIs from the Kotlin Standard Library (including the code generated by the compiler).[4]
In Gradle I’ve configured it as following:
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
apiVersion = "1.2"
languageVersion = "1.2"
}
}
After that, you will probably see some compilation errors. In order to fix them, you should exclude new features usages and write useful extension/function/classes from 1.3 by yourself.
Library development version
You can use the latest Kotlin version in your development. Except for new language features (which we can’t use due to apiVersion and languageVersion) it also brings compiler and tooling improvements.
Dropping support of old versions
In my opinion, it is not healthy to support old versions too long. I would recommend maintaining stability only one version back. Also, you could check release dates in artifacts repository to decide which versions to support [5].
Package structure and visibility
A good library should hide all unnecessary classes, functions and properties from library users. Mostly you should prefer using private and internal modifiers for these situations.
In addition, it is recommended to move all your not public code in the package with name “internal”:
Also, by convention, packages named “internal” are not considered public API. [8]
Java compatibility
In most cases, Kotlin does all work for you. It creates get/setfor properties for example.
Currently, I’ve faced only one problem. If you declare a public property in companion from Java it is accessible via Sample.Companion.INSTANCE
. To fix that use @JvmField
annotation.
class Sample {
companion object {
@JvmField
val INSTANCE = Sample()
}
}
Kotlin dependency in pure Java project
An Android library which is distributed as aar doesn’t include its dependency. Therefore in pure Java project library users will get NoClassDefFoundError
because your library depends on Kotlin std-lib. Thus you should write notices about that in the README file or docs.
But people don’t like to read docs a lot. I would suggest one trick below. Please share in comments your opinion is it a good practice or not?
Declare dependency checker:
public class DependencyChecker {
public static void check() throws ClassNotFoundException {
try {
// Intrinsics exists in all std-lib versions
Class.forName("kotlin.jvm.internal.Intrinsics");
} catch (ClassNotFoundException e) {
throw new ClassNotFoundException("Library depends on Kotlin std-lib.\n" +
"Add to dependencies: org.jetbrains.kotlin:kotlin-stdlib:x.y.z"
);
}
}
}
Check dependency in the static initializer block:
class Sample {
companion object {
init {
DependencyChecker.check()
}
}
}
And if someone forgets to include Kotlin std-lib dependency he will get the solution in a crash log not just error.
Links
- https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#evolving-the-binary-format
- https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#evolving-the-binary-format
- https://kotlinlang.org/docs/reference/using-gradle.html#attributes-common-for-jvm-and-js
- https://kotlinlang.org/docs/reference/evolution/compatibility-modes.html
- https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib
- https://developer.android.com/studio/write/java8-support
- https://stackoverflow.com/a/13550632/7958563
- https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#libraries
Posted on February 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 25, 2024
March 25, 2024