content: add kotlin library tips post [staging]

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2020-11-21 13:54:44 +05:30
parent 4ab3465176
commit f5f323761f
2 changed files with 125 additions and 11 deletions

View File

@ -0,0 +1,125 @@
+++
categories = ["android", "kotlin"]
date = 2020-11-21T00:00:00Z
description = "Building libraries is hard, and keeping track of your public API surface harder. Kotlin 1.4's explicit API mode tries to make the latter not be difficult anymore."
draft = true
slug = "tips-for-building-kotlin-libraries"
social_image = "/uploads/kotlin_800x320.png"
tags = []
title = "Tips and tricks for building libraries in Kotlin"
+++
Building a library is arguably a far more involved task than building an application. You need to be _extra_ mindful of your dependencies, and ensure that you are not breaking source and/or binary compatibility unintentionally. When doing so in Kotlin, you may also need to also provide an idiomatic API surface for Java callers if you're offering JVM support.
I have _some_ experience building libraries, and have had the fortune of seeing a **lot** of other, much smarter people do it. This post aims to serve as a collection of what I've learned by doing things myself and observing others, that will hopefully be helpful to people trying their hand at library development.
## Avoid `data` classes in your public API
Kotlin's [data classes](https://kotlinlang.org/docs/reference/data-classes.html#data-classes) are a fantastic language feature, but unfortunately they pose many challenges. Jake Wharton has written about this in great detail over on [his blog](https://jakewharton.com/public-api-challenges-in-kotlin/), but I will reproduce the problem here as a TL;DR for people who just want to get an overview of the problem.
Here's an example class:
```kotlin
data class Example(
val username: String,
val id: Int,
)
```
Compiling this with `kotlinc` then disassembling it with `javap` gives us this:
```java
public final class Example {
public final java.lang.String getUsername();
public final int getId();
public Example(java.lang.String, int);
public final java.lang.String component1();
public final int component2();
public final Example copy(java.lang.String, int);
public static Example copy$default(Example, java.lang.String, int, int, java.lang.Object);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
}
```
Now, let's add a new field there. The resultant diff will look like this:
```diff
data class Example(
val username: String,
+ val realname: String? = null,
val id: Int,
-)
+) {
+ constructor(username: String, id: Int): this(username, null, id)
+}
```
What we did here was add a secondary constructor with the previous signature, as a way of preserving backwards compatibility. As Jake notes in his article, even this effort from us breaks the public API. Let's compile and disassemble this again to see why.
```diff
Compiled from "Example.kt"
public final class Example {
public final java.lang.String getUsername();
+ public final java.lang.String getRealname();
public final int getId();
+ public Example(java.lang.String, java.lang.String, int);
+ public Example(java.lang.String, java.lang.String, int, int, kotlin.jvm.internal.DefaultConstructorMarker);
public Example(java.lang.String, int);
public final java.lang.String component1();
- public final int component2();
- public final Example copy(java.lang.String, int);
- public static Example copy$default(Example, java.lang.String, int, int, java.lang.Object);
+ public final java.lang.String component2();
+ public final int component3();
+ public final Example copy(java.lang.String, java.lang.String, int);
+ public static Example copy$default(Example, java.lang.String, java.lang.String, int, int, java.lang.Object);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
```
If the problem is not immediately apparent, consider this: `component2()` is no longer returning an `int`. This breaks destructing from Kotlin. The `copy` method's API also changed, which is another binary incompatible change.
You can read more details about how to structure your public classes to avoid this, in Jake's post that I linked above.
## (Ab)use `@SinceKotlin` for offering Java-only APIs
Full disclosure: I picked this up from [LeakCanary](https://github.com/square/leakcanary) so credit goes entirely to [Piwai](https://twitter.com/piwai) for thinking of it.
[`@SinceKotlin`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-since-kotlin/) is an annotation offered in Kotlin that allows things to be marked with the Kotlin version they were first introduced. This allows the usage of classes/methods/properties et al be checked during compile time based on the `-api-version` compiler flag.
For example, if you write this code:
```kotlin
@SinceKotlin("1.4")
class Example(val username: String)
```
and try to compile it like this:
```bash
$ kotlinc example.kt -Werror -api-version 1.3
error: warnings found and -Werror specified
example.kt:1:1: warning: the version is greater than the specified API version 1.3
@SinceKotlin("1.4")
^
```
you can see that compilation fails. I had to pass in `-Werror` manually here, but I believe the Kotlin Gradle Plugin handles making this an error automatically.
How does that help us offer Java-only APIs though? Well, here's how Piwai [did it](https://github.com/square/leakcanary/blob/69d54f36ed9d3204624d214835ba99898665a346/leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt#L177-L184):
```kotlin
/**
* Construct a new Config via [LeakCanary.Config.Builder].
* Note: this method is intended to be used from Java code only. For idiomatic Kotlin use
* `copy()` to modify [LeakCanary.config].
*/
@Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
@SinceKotlin("999.9") // Hide from Kotlin code, this method is only for Java code
fun newBuilder() = Builder(this)
```
Since the version here is set to `999.9`, which hopefully Kotlin will never be over, any attempt to use this will result in a compiler error. The only way to work around this madness is to be equally mad and pass `-api-version 999.9`, which you'd never do, right? 😬

View File

@ -1,11 +0,0 @@
+++
categories = ["android", "kotlin"]
date = 2020-11-03T00:00:00Z
description = "Building libraries is hard, and keeping track of your public API surface harder. Kotlin 1.4's explicit API mode tries to make the latter not be difficult anymore."
draft = true
slug = "kotlin-explicit-api-mode"
social_image = "/uploads/kotlin_800x320.png"
tags = []
title = "Why you should use Kotlin's explicit API mode for your libraries"
+++