Slide 1

Slide 1 text

Building libraries for the next 25 years Martin Bonnin @MartinBonnin

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

"Success Kid" Photograph (c) Laney Griner / Used with Permission

Slide 4

Slide 4 text

● Challenges ● Tips ● What next? Agenda

Slide 5

Slide 5 text

● Challenges ● Tips ● What next?

Slide 6

Slide 6 text

class UserInput( val name: String, val address: String ) mutation UpdateUser($input: UserInput) { updateUser(userInput: $input) { name address } } apollographql/apollo-kotlin

Slide 7

Slide 7 text

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… ?

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

What can break 1/3 Source breaking changes fun greet(name: String): String { return "Hello $name" }

Slide 10

Slide 10 text

What can break 1/3 Source breaking changes fun greet(nickName: String): String { return "Hello $nickName" }

Slide 11

Slide 11 text

What can break 2/3 Binary breaking changes fun greet(name: String): String { return "Hello $name" }

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

App LibA Hello:1.0.0 greet() bye() 1.0.0 LibB 1.0.0

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

Why not both? Source & binary breaking! fun greet(name: String): String { return "Hello $name" }

Slide 16

Slide 16 text

Why not both? Source & binary breaking! fun greet(name: String): String { return "Hello $name" }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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!

Slide 20

Slide 20 text

What can break 3/3 Behaviour breaking changes fun greet(name: String){ return "Helo $name!" } xkcd.com/1319/

Slide 21

Slide 21 text

What can break 3/3 Behaviour breaking changes fun greet(name: String){ return "Helo $name!" return "Hello $name!" } xkcd.com/1319/

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

1. Lots of decisions 2. that are hard to change 3. and easy to break Building libraries is hard!

Slide 27

Slide 27 text

Building libraries is hard & fun! 1. Share best practices 2. Design for evolution 3. Experiment

Slide 28

Slide 28 text

● Challenges ● Tips ● What next?

Slide 29

Slide 29 text

● Challenges ● Tips ○ ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 30

Slide 30 text

● Challenges ● Tips ○ Naming ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 31

Slide 31 text

Use good names!

Slide 32

Slide 32 text

A good name: Tells a story Is unique, Easy to remember, Easy to pronounce Use good names!

Slide 33

Slide 33 text

kotlin-yaml-parser Yams 😃 Yak 😃 Pancakes 😃 😒

Slide 34

Slide 34 text

● Challenges ● Tips ○ Naming ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 35

Slide 35 text

Copy other libs!

Slide 36

Slide 36 text

Okio Extension functions fun File.source(): Source = InputStreamSource(inputStream()) File("/home/martin/kotlinconf.md").source()

Slide 37

Slide 37 text

Okio Extension functions fun File.source(): Source = InputStreamSource(inputStream()) fun Source.buffer(): BufferedSource = RealBufferedSource(this) File("/home/martin/kotlinconf.md").source().buffer().readUtf8()

Slide 38

Slide 38 text

Ktor Factory functions val response = HttpClient().get("https://confetti-app.dev/kotlinconf")

Slide 39

Slide 39 text

Ktor Factory functions fun HttpClient( block: HttpClientConfig.() -> 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")

Slide 40

Slide 40 text

sealed class JsonElement class JsonArray(val content: List) : JsonElement() class JsonObject(val content: Map) : 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() class JsonObject(val content: Map) : JsonElement() class JsonPrimitive(val content: String, val isString: Boolean) : JsonElement() when(Json.parseToJsonElement(string)) { is JsonArray -> // ... is JsonObject -> // ... is JsonPrimitive -> // ... }

Slide 41

Slide 41 text

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" }

Slide 42

Slide 42 text

Managing resources

Slide 43

Slide 43 text

Okio Managing resources inline fun T.use(block: (T) -> R): R {...} File("/home/martin/kotlinconf.md").source().buffer().use { // Do something with the contents }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

fun doSomething(timeout: Long) { // something } doSomething(1000) kotlin-stdlib Value Classes

Slide 46

Slide 46 text

fun doSomething(timeoutMillis: Long) { // something } doSomething(1000) kotlin-stdlib Value Classes

Slide 47

Slide 47 text

kotlin-stdlib Value Classes fun doSomething(timeout: Long, timeUnit: TimeUnit) { // something } doSomething(1, TimeUnit.SECONDS)

Slide 48

Slide 48 text

@JvmInline public value class Duration(private val rawValue: Long) fun doSomething(duration: Duration) { // something } doSomething(1.seconds) kotlin-stdlib Value Classes 😃

Slide 49

Slide 49 text

embeddedServer(Netty, port = 8080) { routing { get("/") { call.respondText("Hello, world!") } } } Ktor Builder DSL

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Java or not java? Java friendly ● Large userbase ● @JvmName ● @JvmStatic ● @JvmOverloads ● etc… Kotlin friendly ● Modern ● Builder DSL ● Coroutines ● Compose ● etc..

Slide 52

Slide 52 text

Okio Extension functions + Java fun File.source(): Source = InputStreamSource(inputStream()) JvmOkioKt.source(new File("/home/martin/kotlinconf.md")); JvmOkio.kt

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Fancy is not always better!

Slide 55

Slide 55 text

// 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

Slide 56

Slide 56 text

Library authors guidelines kotl.in/api-guide

Slide 57

Slide 57 text

Or AndroidX API guidelines github.com/androidx/androidx/tree/androidx-main/docs/api_guidelines

Slide 58

Slide 58 text

● Challenges ● Tips ○ Naming ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 59

Slide 59 text

2021: Sunsetting of jcenter jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Maven Central kittinunf/Result ● Free ● Immutable ● Verifies ownership ● Enforces licenses, signing & more

Slide 62

Slide 62 text

https://oss.sonatype.org/

Slide 63

Slide 63 text

https://s01.oss.sonatype.org/ February 2021

Slide 64

Slide 64 text

February 2021

Slide 65

Slide 65 text

February 2021

Slide 66

Slide 66 text

● Checksums ● Signatures ● Metadata ○ Name ○ Description ○ License ○ Git url ○ Developer ● Source & Javadoc jars ○ May be empty Requirements

Slide 67

Slide 67 text

● Checksums ● Signatures ● Metadata ○ Name ○ Description ○ License ○ Git url ○ Developer ● Source & Javadoc jars ○ May be empty Requirements

Slide 68

Slide 68 text

Feb 2024 central.sonatype.org

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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" ) } } }

Slide 73

Slide 73 text

● Challenges ● Tips ○ Naming ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 74

Slide 74 text

kittinunf/Result Evolve your library Semantic versioning (semver) https://semver.org/ MAJOR.MINOR.PATCH-prerelease ● PATCH => bugfix ● MINOR => new functionality ● MAJOR => breaking changes 💥

Slide 75

Slide 75 text

● “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)

Slide 76

Slide 76 text

Advertise the stability of your alphas It’s OK to use alphas in production

Slide 77

Slide 77 text

Evolve your library Release major versions without breaking changes! ● Change groupId to com.example.greeter2 ● Change package name to greeter2 ● Turn breakage into additions

Slide 78

Slide 78 text

Evolve your symbols @RequiresOptIn @RequiresOptIn(level = RequiresOptIn.Level.WARNING) annotation class ExperimentalApolloApi

Slide 79

Slide 79 text

Evolve your symbols @RequiresOptIn @RequiresOptIn(level = RequiresOptIn.Level.WARNING) annotation class ExperimentalApolloApi

Slide 80

Slide 80 text

Evolve your symbols @RequiresOptIn @RequiresOptIn(level = RequiresOptIn.Level.WARNING) annotation class ExperimentalApolloApi

Slide 81

Slide 81 text

@RequiresOptIn(level = RequiresOptIn.Level.WARNING) annotation class ExperimentalApolloApi @file:OptIn( ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalCoroutinesApi::class, ExperimentalApolloApi::class ) YES, BUT

Slide 82

Slide 82 text

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" }

Slide 83

Slide 83 text

Evolve your symbols @Deprecated

Slide 84

Slide 84 text

Evolve your symbols @Deprecated

Slide 85

Slide 85 text

Evolve your symbols @Deprecated (ERROR) @Deprecated( message = "Use 'flowOn' instead", level = DeprecationLevel.ERROR ) public fun Flow.subscribeOn(context: CoroutineContext): Flow = noImpl()

Slide 86

Slide 86 text

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)

Slide 87

Slide 87 text

Opt-In Stable Deprecated Warning Deprecated Error Deprecated Hidden Removed Symbol lifecycle

Slide 88

Slide 88 text

binary compatibility validator (BCV) Kotlin/binary-compatibility-validator

Slide 89

Slide 89 text

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") }

Slide 90

Slide 90 text

./gradlew apiDump data class Greeting(val name: String, val from: String) public final class Greeting { public fun (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; }

Slide 91

Slide 91 text

./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; }

Slide 92

Slide 92 text

./gradlew apiCheck fun greet(nickName: String): String { return "Hello $nickName" } BUILD SUCCESSFUL in 985ms 4 actionable tasks: 4 executed

Slide 93

Slide 93 text

./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; }

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

./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; }

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

apiValidation { @OptIn(kotlinx.validation.ExperimentalBCVApi::class) klib { enabled = true } } ✨ BCV klib support

Slide 99

Slide 99 text

apiValidation { @OptIn(kotlinx.validation.ExperimentalBCVApi::class) klib { enabled = true } } ✨ BCV klib support

Slide 100

Slide 100 text

🌶 Skip explicitApi kotlin { explicitApi() }

Slide 101

Slide 101 text

🌶 Skip explicitApi Is this needed?

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

● Challenges ● Tips ○ Naming ○ API Design ○ Publishing ○ Evolution ● What next?

Slide 104

Slide 104 text

Error handling

Slide 105

Slide 105 text

Google SDK index

Slide 106

Slide 106 text

Error handling ● Programming errors ○ throw ● Domain errors ○ typed https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07

Slide 107

Slide 107 text

But kotlin.Result arrow.core.Either arrow.core.raise.Raise kittinunf/Result michaelbull/kotlin-result kittinunf/Result Your own? skydoves/sandwich fork-handles/result4k

Slide 108

Slide 108 text

KT-68296 Union Types for Errors // getOrThrow -> get fun get(): T | Error // maxOrNull -> max fun IntArray.max(): Int | NoSuchElement // awaitSingleOrNull -> awaitSingle fun awaitSingle(): T | NoSuchElement

Slide 109

Slide 109 text

● BCV ● Dokka ● Publishing ● Java/Kotlin compatibility ● Maven compatibility ● Configuration cache ● Project isolation ● etc… Gradle

Slide 110

Slide 110 text

kotl.in/slack #naming #opensource #library-development Join the fun

Slide 111

Slide 111 text

Takeaways ● Resources ○ API guidelines ○ Vanniktech’s ○ BCV ○ Community ♥ ● You can never love your libraries too much ● See you at KotlinConf 2049!

Slide 112

Slide 112 text

Thank you, and don’t forget to vote @MartinBonnin