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

Building libraries for the next 25 years

Building libraries for the next 25ย years

mbonnin

May 27, 2024
Tweet

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 => bug๏ฌx โ€ข 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 ๏ฌle โ€ข 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 ๏ฌle โ€ข 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 ๏ฌle โ€ข 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 โ€ข Con๏ฌguration 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!