Slide 1

Slide 1 text

Integrating Kotlin/JS in a Kotlin Multiplatform library Jig a r Br a hmbh a tt (@sh a ktim a n_droid) | Droidcon NY 2022

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

A bit of history • Started integrating Kotlin/JS in production in late 2021 • Started bumpy Kotlin/JS ride with Kotlin 1.5.32 • Not many online resources to refer • Kotlin docs & Kotlin Slack #Javascript channel ❤ • Kotlin 1.6.20 & 1.7.10 made things easier • Still long way to go!

Slide 4

Slide 4 text

What is Kotlin/JS

Slide 5

Slide 5 text

Kotlin/JS => Kotlin for JavaScript • JavaScript - One of the supported platforms in Kotlin ecosystem • Kotlin code transpiles into JS (Write in Kotlin = Output in JS) • Supports • Does lot of heavy lifting behind the scene (npm, yarn, webpack etc.) • Newbie approved 👍 • Solid gradle plugin

Slide 6

Slide 6 text

Set up Kotlin/JS

Slide 7

Slide 7 text

Set up Kotlin/JS sh a red/build.gr a dle.kts 🤔

Slide 8

Slide 8 text

Set up Kotlin/JS Most likely next error!

Slide 9

Slide 9 text

Set up Kotlin/JS Most likely next error! // root - > build.gradle.kts tasks.register("clean", Delete :: class) { delete(rootProject.buildDir) } Remove this!

Slide 10

Slide 10 text

Set up Kotlin/JS sh a red/build.gr a dle.kts (Option a l Step if hier a rchic a l project structure support en a bled)

Slide 11

Slide 11 text

Set up Kotlin/JS sh a red/src

Slide 12

Slide 12 text

Set up Kotlin/JS sh a red/src

Slide 13

Slide 13 text

Set up Kotlin/JS Existing Kotlin Multiplatform Mobile Project? Build would likely fail!

Slide 14

Slide 14 text

One more step - Two options! Set up Kotlin/JS

Slide 15

Slide 15 text

Set up Kotlin/JS Option 1: • Refactor code in commonMain so that it’s compatible with JS • What if a some code or library doesn’t support JS target?

Slide 16

Slide 16 text

Set up Kotlin/JS It can be overwhelming with a lot of upfront work!” Option 1: • Refactor code in commonMain so that it’s compatible with JS • What if a some code or library doesn’t support JS target?

Slide 17

Slide 17 text

Set up Kotlin/JS Option 2: • Introduce a common sourceSet for mobile platforms • JS/web on the other hand can be somewhat di ff erent • mobileMain allows separation of features & dependencies • Native mobile platforms has similar needs and capabilities mobileMain iosMain androidMain

Slide 18

Slide 18 text

Set up Kotlin/JS Option-2 steps: • Add mobileMain sourceSet • Move entire commonMain to mobileMain • Now slowly add JS support for existing features 👶🪜

Slide 19

Slide 19 text

sourceSets { val commonMain by … val commonTest by … val androidMain by … val androidTest by … val iosMain by … val iosTest by … + val mobileMain by creating { dependsOn(commonMain) + androidMain.dependsOn(this) + iosMain.dependsOn(this) } + val mobileTest by creating { dependsOn(commonTest) + androidTest.dependsOn(this) + iosTest.dependsOn(this) } val jsMain by getting val jsTest by getting } Set up Kotlin/JS sh a red/build.gr a dle.kts

Slide 20

Slide 20 text

Set up Kotlin/JS sh a red/src

Slide 21

Slide 21 text

JS build output

Slide 22

Slide 22 text

JS build output

Slide 23

Slide 23 text

compiler LEGACY - not recommended JS build output

Slide 24

Slide 24 text

# Browser as JS Target # Likely usage: Web App/Module # kotlin-stdlib-js JS build output

Slide 25

Slide 25 text

# Node.js as JS Target # Likely usage: Node Application/Module # kotlinx-nodejs:0.0.7 (experimental) JS build output

Slide 26

Slide 26 text

# Outputs JS as an executable # Dead code elimination (DCE) JS build output # bundle fi le

Slide 27

Slide 27 text

# Outputs JS as a library module # Experimental Typescript support (.d.ts) # https://github.com/mpetuska/npm-publish JS build output

Slide 28

Slide 28 text

JS build output bin a ries.execut a ble() ./gradlew jsBrowserDistribution (production) ./gradlew jsBrowserDevelopmentWebpack (faster, no dce)

Slide 29

Slide 29 text

JS build output bin a ries.libr a ry() ./gradlew jsBrowserProductionLibraryDistribution (production)

Slide 30

Slide 30 text

How to expose Kotlin to JS?

Slide 31

Slide 31 text

// commonMain class Greeting { fun greeting(): String { return "Hello, ${Platform().name}!" } } // jsMain actual class Platform actual constructor() { actual val name: String get() = "JS" } How to expose Kotlin to JS?

Slide 32

Slide 32 text

// commonMain @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().name}!" } } How to expose Kotlin to JS?

Slide 33

Slide 33 text

/ / commonMain @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().name}!" } } export namespace shaktiman.droid.nydroidcon22 { class Greeting { constructor(); greeting(): string; } } export as namespace kmp_js_lib; .d.ts fi le function Greeting() { } Greeting.prototype.greeting = function () { return 'Hello, ' + (new Platform()).a() + '!'; }; Greeting.$metadata$ = classMeta('Greeting'); function Platform() { } Platform.prototype.a = function () { return 'JS'; }; Platform.$metadata$ = classMeta('Platform'); .js fi le How to expose Kotlin to JS?

Slide 34

Slide 34 text

Us a ge // JavaScript usage import sdk from 'kmp-lib'; const greeting = new sdk.shaktiman.droid.nydroidcon22.Greeting() console.log(greeting.greeting()) // TypeScript usage import * as sdk from 'kmp-lib'; let greeting = new sdk.shaktiman.droid.nydroidcon22.Greeting() console.log(greeting.greeting()) How to expose Kotlin to JS?

Slide 35

Slide 35 text

@JsExport data class Address(val address1: String, val city: String) How to expose Kotlin to JS? All references needs to be export a ble

Slide 36

Slide 36 text

@JsExport Challenges: • Not everything is JsExportable yet, or still buggy! How to expose Kotlin to JS? • No support for exporting collections API (Map/List) • In common code, you would have to use Array In public API • Suspended functions cannot be exported (use Promise)

Slide 37

Slide 37 text

@JsExport Challenges: • Not everything is JsExportable yet, or still buggy! • No support for collections API (Map/List) • In common code, you would have to use Array In public API • Suspended functions cannot be exported (use Promise) How to expose Kotlin to JS?

Slide 38

Slide 38 text

How to expose Kotlin to JS? @JsExport Positives: • Kotlin 1.6.20 - Enum and interfaces got 100% supported • Experimental Typescript support has improved over last two releases • Kotlin wrappers lib is great for providing wrappers around JS objects (https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-js) • KustomExport library uses KSP to provide missing functionality (https://github.com/deezer/KustomExport)

Slide 39

Slide 39 text

How to expose Kotlin to JS? @JsN a me @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().platform}!" } fun greeting(version: String): String { return "${greeting()} Version 1.7.10" } }

Slide 40

Slide 40 text

How to expose Kotlin to JS? @JsN a me @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().platform}!" } fun greeting(version: String): String { return "${greeting()} Version 1.7.10" } }

Slide 41

Slide 41 text

How to expose Kotlin to JS? @JsN a me @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().platform}!" } @JsName("greetingWithVersion") fun greeting(version: String): String { return "${greeting()} Version 1.7.10" } }

Slide 42

Slide 42 text

How to expose Kotlin to JS? @JsN a me @JsExport class Greeting { fun greeting(): String { return "Hello, ${Platform().platform}!" } @JsName("greetingWithVersion") fun greeting(version: String): String { return "${greeting()} Version 1.7.10" } } // Generated Typescript definition export namespace shaktiman.droid.kmptrial2 { class Greeting { constructor(); greeting(): string; greetingWithVersion(version: string): string; } }

Slide 43

Slide 43 text

How to expose Kotlin to JS?

Slide 44

Slide 44 text

What about third party dependencies?

Slide 45

Slide 45 text

If a library is already Multiplatform with JS support val commonMain by getting { dependencies { implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x") } } If you want to use a JS speci fi c library val jsMain by getting { dependencies { implementation(npm("launchdarkly-js-client-sdk", “x.x.x")) } } third p a rty dependencies

Slide 46

Slide 46 text

You’d need to write external declarations to use an NPM module in Kotlin @JsModule("launchdarkly-js-client-sdk") external fun initialize(envKey: String, user: LDUser, options: LDOptions): LDClient Need help with generating external declarations? third p a rty dependencies

Slide 47

Slide 47 text

implementation(npm(“", “x.x.x”, generateExternals = true)) third p a rty dependencies Looks for Typescript de fi nitions fi le Copy what you need → Paste in your module → Fix if required → Remove “generateExternal = true”

Slide 48

Slide 48 text

third p a rty dependencies

Slide 49

Slide 49 text

third p a rty dependencies

Slide 50

Slide 50 text

third p a rty dependencies

Slide 51

Slide 51 text

third p a rty dependencies

Slide 52

Slide 52 text

third p a rty dependencies

Slide 53

Slide 53 text

So far •Setting up Kotlin/JS ✅ •JS output as an executable vs a library ✅ •@JSExport and external declarations ✅

Slide 54

Slide 54 text

How to go about Multiplatform?

Slide 55

Slide 55 text

How to go about Multiplatform? • Coroutines | Serialization | Datetime (@Kotlinx) • Ktor (@ktorio) • SQLDelight (@cashapp) • Apollo Kotlin (@apollographql) • Multiplatform Settings (@russhwolf) • Kermit (@touchlab) • UUID (@benasher44) A lot of popul a r Multipl a tform libr a ries h a ve JS suport

Slide 56

Slide 56 text

But for JS, bundle size matters!

Slide 57

Slide 57 text

Size imp a ct comp a rison Without any libs (Just one method in commonMain) Kotlin 1.6.10 780 bytes With Kotlinx.datetime (Adds Joda-Time) 202 KB Version 0.3.2 With Kotlinx.serialization (With one serializable class) With Kotlinx.serialization (With two serializable classes) With Kotlinx.coroutines With Kotlinx.Ktor (Includes coroutine-core) All combined (~ 50 lines of code) 1.3 MB 300 KB Version 1.3.2 303 KB 122 KB Version 1.6.1 862 KB Version 2.00-beta-1 - Ran ./gradlew jsBrowserDistribution (So after DCE sizes) - Code available at https://github.com/touchlab-lab/kotlin-js-droidcon-22 Kotlin 1.6.20 796 bytes 204 KB Version 0.3.2 1.1 MB 332 KB Version 1.3.2 336 KB 127 KB Version 1.6.4 683 KB Version 2.0.3 Kotlin 1.7.10 1277 bytes 202 KB Version 0.4.0 803 KB 238 KB Version 1.4.0 303 KB 92 KB Version 1.6.4 465 KB Version 2.1.0 (~40% reducation 😍)

Slide 58

Slide 58 text

Size Issue - Work a round - D a tetime // commonMain expect class Instant : Comparable { val epochSeconds: Long val nanosecondsOfSecond: Int override operator fun compareTo(other: Instant): Int override fun toString(): String fun toEpochMilliseconds(): Long companion object { fun parse(isoString: String): Instant fun fromEpochMilliseconds(epochMilliseconds: Long): Instant } } // mobileMain actual typealias Instant = kotlinx.datetime.Instant // jsMain import kotlin.js.Date actual class Instant( actual val epochSeconds: Long, actual val nanosecondsOfSecond: Int ) : Comparable { // Implementation }

Slide 59

Slide 59 text

Size Issue - Work a round - Seri a liz a tion // commonMain - Serializable.kt @OptIn(ExperimentalMultiplatform :: class) @OptionalExpectation @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS, AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) expect annotation class Serializable() // mobileMain - Serializable.kt actual typealias Serializable = kotlinx.serialization.Serializable // commonMain @Serializable data class Person(val id: String, val address: Address) // commonMain expect inline fun encodeThisToString(value: T): String expect inline fun decodeThisFromString(value: String): T

Slide 60

Slide 60 text

Size Issue - Work a round - HttpClient // commonMain internal interface KmpHttpClient { suspend fun request(url: String, block: suspend KmpRequestBuilder.() - > Unit): KmpHttpResponse } internal data class KmpHttpResponse(val body: String?) internal interface KmpRequestBuilder { var method: String val headersBuilder: Map var body: Any } // jsMain - Using window.fetch API internal class JsHttpClient : IdentityHttpClient { // Implementation } // mobileMain - Using ktor API internal class KtorHttpClient(engine: HttpClientEngine) : IdentityHttpClient { // Implementation }

Slide 61

Slide 61 text

Size Issue - Work a round - Api Impl // commonMain - AstrosApi.kt @Serializable @JsExport data class AstrosApiResponse(val number: Int, val people: Array, val message: String) @Serializable @JsExport data class People(val name: String, val craft: String) internal class AstrosApi(private val httpClient: KmpHttpClient) { suspend fun getAstros(): AstrosApiResponse? { val response = httpClient.request(BASE_URL) { method = "GET" }.body return response ? . let {decodeThisFromString(it) } } }

Slide 62

Slide 62 text

Size Issue - Work a round - Api Impl // jsMain @JsExport object AstrosApiWrapper { private val astrosApi = AstrosApi(JsHttpClient()) fun getAstros(): Promise = GlobalScope.promise { astrosApi.getAstros() } }

Slide 63

Slide 63 text

How to go about Multiplatform? • Re-think about architecture with JS in mind Things to keep in mind • Keep checking on JS library size • Write JS only facade layer • Be prepared for surprises and disappointment

Slide 64

Slide 64 text

Buzz (Kotlin Slack)

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

Thank you Droidcon NYC Jig a r Br a hmbh a tt 
 @sh a ktim a n_droid (Kotlin Sl a ck & Twitter)