Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building libraries for the next 25 years

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →

Building libraries for the next 25 years

Avatar for mbonnin

mbonnin

May 27, 2024

More Decks by mbonnin

Other Decks in Technology

Transcript

  1. class UserInput( val name: String, val address: String ) mutation

    UpdateUser($input: UserInput) { updateUser(userInput: $input) { name address } } apollographql/apollo-kotlin
  2. Lots of choices • What name to use? • How

    do I design my API? • Coroutines? • Builders? Or DSL? • Where to publish? • Should it be one big artifact or several smaller? • Publish kdocs? Sources? Signatures? • What versioning strategy? • Am I making a breaking change? • What versions of Kotlin to support? • And Gradle? • And Maven? • Should this throw? • How to do I/O in JS? • How to model websocket backpressure? • etc… ?
  3. What can break 2/3 Binary breaking changes fun greet(name: String,

    from: String = "Copenhagen"): String { return "Hello $name from $from!" } Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.String GreetKt.greet(java.lang.String)' at MainKt.main(Main.kt:3) at MainKt.main(Main.kt)
  4. App LibA Hello:1.1.0 greet() bye() 1.0.0 LibB:n+1 1.1.0 Exception in

    thread "main" java.lang.NoSuchMethodError: 'java.lang.String GreetKt.greet(java.lang.String)' at MainKt.main(Main.kt:3) at MainKt.main(Main.kt)
  5. Terminology Source breaking change • Breaks the API (Application Programming

    Interface) • Noticed at build time Binary breaking change • Breaks the ABI (Application Binary Interface) • Noticed at run time
  6. Terminology Source breaking change • Breaks the API (Application Programming

    Interface) • Noticed at build time • Breaking it is bad! Binary breaking change • Breaks the ABI (Application Binary Interface) • Noticed at run time
  7. Terminology Source breaking change • Breaks the API (Application Programming

    Interface) • Noticed at build time • Breaking it is bad! Binary breaking change • Breaks the ABI (Application Binary Interface) • Noticed at run time • Breaking it is worse!
  8. What can break 3/3 Behaviour breaking changes fun greet(name: String){

    return "Helo $name!" return "Hello $name!" } xkcd.com/1319/
  9. What can break 4/3 Publication changes . └── com └──

    greeter ├── 0.0.1 │ ├── greeter-0.0.1.jar │ ├── greeter-0.0.1.module │ └── greeter-0.0.1.pom └── maven-metadata-local.xml
  10. What can break 4/3 Publication changes . └── com ├──

    greeter │ ├── 0.0.1 │ │ ├── greeter-0.0.1-kotlin-tooling-metadata.json │ │ ├── greeter-0.0.1-sources.jar │ │ ├── greeter-0.0.1.jar │ │ ├── greeter-0.0.1.module │ │ └── greeter-0.0.1.pom │ └── maven-metadata-local.xml ├── greeter-jvm │ ├── 0.0.1 │ │ ├── greeter-jvm-0.0.1-sources.jar │ │ ├── greeter-jvm-0.0.1.jar │ │ ├── greeter-jvm-0.0.1.module │ │ └── greeter-jvm-0.0.1.pom │ └── maven-metadata-local.xml └── greeter-macosarm64 ├── 0.0.1 │ ├── greeter-macosarm64-0.0.1-metadata.jar │ ├── greeter-macosarm64-0.0.1-sources.jar │ ├── greeter-macosarm64-0.0.1.klib
  11. What can break 4/3 Publication changes . └── com ├──

    greeter │ ├── 0.0.1 │ │ ├── greeter-0.0.1-kotlin-tooling-metadata.json │ │ ├── greeter-0.0.1-sources.jar │ │ ├── greeter-0.0.1.jar │ │ ├── greeter-0.0.1.module │ │ └── greeter-0.0.1.pom │ └── maven-metadata-local.xml ├── greeter-jvm │ ├── 0.0.1 │ │ ├── greeter-jvm-0.0.1-sources.jar │ │ ├── greeter-jvm-0.0.1.jar │ │ ├── greeter-jvm-0.0.1.module │ │ └── greeter-jvm-0.0.1.pom │ └── maven-metadata-local.xml └── greeter-macosarm64 ├── 0.0.1 │ ├── greeter-macosarm64-0.0.1-metadata.jar │ ├── greeter-macosarm64-0.0.1-sources.jar │ ├── greeter-macosarm64-0.0.1.klib
  12. What can break 4/3 Publication changes . └── com ├──

    greeter │ ├── 0.0.1 │ │ ├── greeter-0.0.1-kotlin-tooling-metadata.json │ │ ├── greeter-0.0.1-sources.jar │ │ ├── greeter-0.0.1.jar │ │ │ ├── META-INF │ │ │ │ ├── MANIFEST.MF │ │ │ │ └── kotlin-project-structure-metadata.json │ │ │ └── commonMain │ │ │ └── default │ │ │ ├── linkdata │ │ │ │ ├── module │ │ │ │ └── root_package │ │ │ │ └── 0_.knm │ │ │ ├── manifest │ │ │ └── resources │ │ ├── greeter-0.0.1.module │ │ └── greeter-0.0.1.pom │ └── maven-metadata-local.xml ├── greeter-jvm
  13. 1. Lots of decisions 2. that are hard to change

    3. and easy to break Building libraries is hard!
  14. Building libraries is hard & fun! 1. Share best practices

    2. Design for evolution 3. Experiment
  15. • Challenges • Tips ◦ Naming ◦ API Design ◦

    Publishing ◦ Evolution • What next?
  16. A good name: Tells a story Is unique, Easy to

    remember, Easy to pronounce Use good names!
  17. • Challenges • Tips ◦ Naming ◦ API Design ◦

    Publishing ◦ Evolution • What next?
  18. Okio Extension functions fun File.source(): Source = InputStreamSource(inputStream()) fun Source.buffer():

    BufferedSource = RealBufferedSource(this) File("/home/martin/kotlinconf.md").source().buffer().readUtf8()
  19. Ktor Factory functions fun <T : HttpClientEngineConfig> HttpClient( block: HttpClientConfig<T>.()

    -> Unit = {} ): HttpClient { val engine = engineFactory.create(config.engineConfig) val client = HttpClient(engine, config, manageEngine = true) // ... return client } val response = HttpClient().get("https://confetti-app.dev/kotlinconf")
  20. sealed class JsonElement class JsonArray(val content: List<JsonElement>) : JsonElement() class

    JsonObject(val content: Map<String, JsonElement>) : JsonElement() class JsonPrimitive(val content: String, val isString: Boolean) : JsonElement() class JsonPair(val first: JsonElement, val second: JsonElement) : JsonElement() when(Json.parseToJsonElement(string)) { is JsonArray -> // ... is JsonObject -> // ... is JsonPrimitive -> // ... } kotlinx-serialization Sealed Classes sealed class JsonElement class JsonArray(val content: List<JsonElement>) : JsonElement() class JsonObject(val content: Map<String, JsonElement>) : JsonElement() class JsonPrimitive(val content: String, val isString: Boolean) : JsonElement() when(Json.parseToJsonElement(string)) { is JsonArray -> // ... is JsonObject -> // ... is JsonPrimitive -> // ... }
  21. Okio Operator overloading class Path { fun resolve(child: String, normalize:

    Boolean): Path { ... } } fun kotlinConfNotes(home: Path): Path { return home.resolve("kotlinconf.md") } class Path { fun resolve(child: String, normalize: Boolean): Path { ... } operator fun div(child: String): Path = commonResolve(child) } fun kotlinConfNotes(home: Path): Path { return home / "kotlinconf.md" }
  22. Okio Managing resources inline fun <T : Closeable?, R> T.use(block:

    (T) -> R): R {...} File("/home/martin/kotlinconf.md").source().buffer().use { // Do something with the contents }
  23. Okio and kotlin-stdlib Managing resources inline fun <T : AutoCloseable?,

    R> T.use(block: (T) -> R): R {...} File("/home/martin/kotlinconf.md").source().buffer().use { // Do something with the contents }
  24. @JvmInline public value class Duration(private val rawValue: Long) fun doSomething(duration:

    Duration) { // something } doSomething(1.seconds) kotlin-stdlib Value Classes 😃
  25. embeddedServer(Netty, port = 8080) { routing { get("/") { call.respondText("Hello,

    world!") } } } Ktor Builder DSL EmbeddedServer.Builder(Netty, port = 8080) .routing( Routing.Builder() .add("/", GET) { call.respondText("Hello, world!") } .build() ).build() Without DSL
  26. Java or not java? Java friendly • Large userbase •

    @JvmName • @JvmStatic • @JvmOverloads • etc… Kotlin friendly • Modern • Builder DSL • Coroutines • Compose • etc..
  27. Okio Extension functions + Java fun File.source(): Source = InputStreamSource(inputStream())

    JvmOkioKt.source(new File("/home/martin/kotlinconf.md")); JvmOkio.kt
  28. Okio Extension functions + Java @file:JvmMultifileClass @file:JvmName("Okio") fun File.source(): Source

    = InputStreamSource(inputStream()) Okio.source(new File("/home/martin/kotlinconf.md")); JvmOkio.kt
  29. // Same thing dependencies { add("implementation", "io.ktor:ktor-client-core:2.3.11") } Fancy is

    not always better! dependencies { "implementation"("io.ktor:ktor-client-core:2.3.11") } operator fun String.invoke(dependencyNotation: Any): Dependency
  30. • Challenges • Tips ◦ Naming ◦ API Design ◦

    Publishing ◦ Evolution • What next?
  31. kittinunf/Result . └── com └── example └── greeter ├── 0.0.1

    │ ├── greeter-0.0.1.jar │ ├── greeter-0.0.1.module │ └── greeter-0.0.1.pom └── maven-metadata-local.xml Maven Central
  32. • Checksums • Signatures • Metadata ◦ Name ◦ Description

    ◦ License ◦ Git url ◦ Developer • Source & Javadoc jars ◦ May be empty Requirements
  33. • Checksums • Signatures • Metadata ◦ Name ◦ Description

    ◦ License ◦ Git url ◦ Developer • Source & Javadoc jars ◦ May be empty Requirements
  34. kittinunf/Result android { publishing { singleVariant("release") { withSourcesJar() } }

    } ✨ publishing { publications.create("default", MavenPublication::class.java) { from(components.getByName("java")) artifact(javaSourceTask) } } Android Kotlin JVM Kotlin multiplatform
  35. Ship Use Gradle Maven Publish Plugin plugins { id("com.vanniktech.maven.publish") version

    "0.28.0" } mavenPublishing { publishToMavenCentral(SonatypeHost.DEFAULT) // or when publishing to https://s01.oss.sonatype.org publishToMavenCentral(SonatypeHost.S01) // or when publishing to https://central.sonatype.com/ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) } https://github.com/vanniktech/gradle-maven-publish-plugin
  36. Ship Use Vanniktech’s Plugin plugins { id("com.vanniktech.maven.publish") version "0.28.0" }

    mavenPublishing { publishToMavenCentral(SonatypeHost.DEFAULT) // or when publishing to https://s01.oss.sonatype.org publishToMavenCentral(SonatypeHost.S01) // or when publishing to https://central.sonatype.com/ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) } https://github.com/vanniktech/gradle-maven-publish-plugin
  37. Ship Host your SNAPSHOTs/nightlies publishing { repositories { maven {

    /** * Upload to google cloud * environment variable: * GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json */ name = "gcs" setUrl("gcs://my-gcp-bucket/m2" ) } } }
  38. • Challenges • Tips ◦ Naming ◦ API Design ◦

    Publishing ◦ Evolution • What next?
  39. kittinunf/Result Evolve your library Semantic versioning (semver) https://semver.org/ MAJOR.MINOR.PATCH-prerelease •

    PATCH => bugfix • MINOR => new functionality • MAJOR => breaking changes 💥
  40. • “I broke everything last night” • Bugs 🐛 •

    Tested • Feature complete • Documentation • API is stable • Migration guide • Battle tested 💪 • Long term support kittinunf/Result 0.x.y 1.0.0 1.x.y 2.0.0-alpha.x 2.0.0-beta.x 2.0.0-rc.x 2.0.0 2.0.1 • • • • • • • • Evolve your library Semantic versioning (semver)
  41. Evolve your library Release major versions without breaking changes! •

    Change groupId to com.example.greeter2 • Change package name to greeter2 • Turn breakage into additions
  42. Evolve your symbols @Deprecated @Deprecated( "Use greet(name, locale) instead", ReplaceWith(

    "greet(name, Locale.ENGLISH)", "java.util.Locale" ) ) fun greet(name: String): String { return "Hello $name" }
  43. Evolve your symbols @Deprecated (ERROR) @Deprecated( message = "Use 'flowOn'

    instead", level = DeprecationLevel.ERROR ) public fun <T> Flow<T>.subscribeOn(context: CoroutineContext): Flow<T> = noImpl()
  44. Evolve your symbols @Deprecated (HIDDEN) @Deprecated( message = "This overload

    is only kept for binary compatibility", level = DeprecationLevel.HIDDEN ) public fun parse(isoString: String): LocalDateTime = parse(input = isoString)
  45. Monitor your API binary-compatibility-validator (BCV) • Tracks the public ABI

    • apiDump: dumps the ABI to a file • apiCheck: checks that the ABI did not change plugins { id("org.jetbrains.kotlinx.binary-compatibility-validator").version("0.15.0-Beta.2") }
  46. ./gradlew apiDump data class Greeting(val name: String, val from: String)

    public final class Greeting { public fun <init> (Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun copy (Ljava/lang/String;Ljava/lang/String;)LGreeting; public static synthetic fun copy$default (LGreeting;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)LGreeting; public fun equals (Ljava/lang/Object;)Z public final fun getFrom ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; }
  47. ./gradlew apiDump fun greet(name: String): String { return "Hello $name"

    } public final class GreetKt { public static final fun greet (Ljava/lang/String;)Ljava/lang/String; }
  48. ./gradlew apiCheck fun greet(nickName: String): String { return "Hello $nickName"

    } BUILD SUCCESSFUL in 985ms 4 actionable tasks: 4 executed
  49. ./gradlew apiCheck fun greet(name: String, from: String = "Copenhagen"): String

    { return "Hello $name from $from" } Execution failed for task ':apiCheck'. > API check failed for project greeter. --- /Users/mbonnin/git/greeter/api/greeter.api +++ /Users/mbonnin/git/greeter/build/api/greeter.api @@ -1,5 +1,6 @@ public final class GreetKt { - public static final fun greet (Ljava/lang/String;)Ljava/lang/String; + public static final fun greet (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public static synthetic fun greet$default (Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; }
  50. ./gradlew apiCheck fun greet(name: String): String { return "Hello $name"

    } fun byebye(name: String): String { return "Bye bye $name" } Execution failed for task ':apiCheck'. > API check failed for project greeter. --- /Users/mbonnin/git/greeter/api/greeter.api +++ /Users/mbonnin/git/greeter/build/api/greeter.api @@ -1,4 +1,5 @@ public final class GreetKt { + public static final fun byebye(Ljava/lang/String;)Ljava/lang/String; public static final fun greet(Ljava/lang/String;)Ljava/lang/String; }
  51. Monitor your API binary-compatibility-validator (BCV) • Tracks the public ABI

    • apiDump: dumps the ABI to a file • apiCheck: checks that the ABI did not change
  52. Monitor your API binary-compatibility-validator (BCV) • Tracks the public ABI

    • apiDump: dumps the ABI to a file • apiCheck: checks that the ABI did not change
  53. We’ve gone a long way! Feb 2016 Kotlin 1.0 Feb

    2020 BCV Mar 2023 Library guidelines April 2024 Klib support in BCV Feb 2021 Jcenter sunset
  54. • Challenges • Tips ◦ Naming ◦ API Design ◦

    Publishing ◦ Evolution • What next?
  55. Error handling • Programming errors ◦ throw • Domain errors

    ◦ typed https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07
  56. KT-68296 Union Types for Errors // getOrThrow -> get fun

    <T> get(): T | Error // maxOrNull -> max fun IntArray.max(): Int | NoSuchElement // awaitSingleOrNull -> awaitSingle fun <T> awaitSingle(): T | NoSuchElement
  57. • BCV • Dokka • Publishing • Java/Kotlin compatibility •

    Maven compatibility • Configuration cache • Project isolation • etc… Gradle
  58. Takeaways • Resources ◦ API guidelines ◦ Vanniktech’s ◦ BCV

    ◦ Community ♥ • You can never love your libraries too much • See you at KotlinConf 2049!