DEV Community

Cover image for Kotlin library development for Android/Java hints
Ivan Shafran
Ivan Shafran

Posted on • Edited on

Kotlin library development for Android/Java hints

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Check dependency in the static initializer block:

class Sample {
    companion object {
        init {
            DependencyChecker.check()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And if someone forgets to include Kotlin std-lib dependency he will get the solution in a crash log not just error.

Links

  1. https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#evolving-the-binary-format
  2. https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#evolving-the-binary-format
  3. https://kotlinlang.org/docs/reference/using-gradle.html#attributes-common-for-jvm-and-js
  4. https://kotlinlang.org/docs/reference/evolution/compatibility-modes.html
  5. https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib
  6. https://developer.android.com/studio/write/java8-support
  7. https://stackoverflow.com/a/13550632/7958563
  8. https://kotlinlang.org/docs/reference/evolution/kotlin-evolution.html#libraries

Top comments (0)

The discussion has been locked. New comments can't be added.