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

Integrating Kotlin/JS in a KMP library

Integrating Kotlin/JS in a KMP library

Presented at Droidcon NYC, 2022

https://nyc.droidcon.com/jigar-brahmbhatt/

Javascript is one of the supported targets for a Kotlin Multiplatform project. One can add Kotlin/JS target to an existing Kotlin Multiplatform Mobile library to output a JS library code.

There are various aspects to consider for the Kotlin/JS integration,
• Supporting JavaScript or Typescript
• Gradle configuration
• Exposing Kotlin code to JavaScript and writing JS-specific code
• JS Library size and impact of third-party libraries
• Architectural decisions around shared code across platforms, networking, storage, serialization, etc.

This talk discusses how to deal with the issues mentioned above and more.

Jigar Brahmbhatt

September 01, 2022
Tweet

More Decks by Jigar Brahmbhatt

Other Decks in Programming

Transcript

  1. 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
  2. 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!
  3. 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
  4. Set up Kotlin/JS Most likely next error! // root -

    > build.gradle.kts tasks.register("clean", Delete :: class) { delete(rootProject.buildDir) } Remove this!
  5. 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)
  6. 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?
  7. 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?
  8. 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
  9. Set up Kotlin/JS Option-2 steps: • Add mobileMain sourceSet •

    Move entire commonMain to mobileMain • Now slowly add JS support for existing features 👶🪜
  10. 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
  11. # Browser as JS Target # Likely usage: Web App/Module

    # kotlin-stdlib-js JS build output
  12. # Node.js as JS Target # Likely usage: Node Application/Module

    # kotlinx-nodejs:0.0.7 (experimental) JS build output
  13. # Outputs JS as an executable # Dead code elimination

    (DCE) JS build output # bundle fi le
  14. # Outputs JS as a library module # Experimental Typescript

    support (.d.ts) # https://github.com/mpetuska/npm-publish JS build output
  15. JS build output bin a ries.execut a ble() ./gradlew jsBrowserDistribution

    (production) ./gradlew jsBrowserDevelopmentWebpack (faster, no dce)
  16. // 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?
  17. // commonMain @JsExport class Greeting { fun greeting(): String {

    return "Hello, ${Platform().name}!" } } How to expose Kotlin to JS?
  18. / / 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?
  19. 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?
  20. @JsExport data class Address(val address1: String, val city: String) How

    to expose Kotlin to JS? All references needs to be export a ble
  21. @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)
  22. @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?
  23. 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)
  24. 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" } }
  25. 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" } }
  26. 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" } }
  27. 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; } }
  28. 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
  29. 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
  30. implementation(npm(“<package name>", “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”
  31. So far •Setting up Kotlin/JS ✅ •JS output as an

    executable vs a library ✅ •@JSExport and external declarations ✅
  32. 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
  33. 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 😍)
  34. Size Issue - Work a round - D a tetime

    // commonMain expect class Instant : Comparable<Instant> { 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<Instant> { // Implementation }
  35. 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 <reified T> encodeThisToString(value: T): String expect inline fun <reified T> decodeThisFromString(value: String): T
  36. 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<String, Any> var body: Any } // jsMain - Using window.fetch API internal class JsHttpClient : IdentityHttpClient { // Implementation } // mobileMain - Using ktor API internal class KtorHttpClient(engine: HttpClientEngine) : IdentityHttpClient { // Implementation }
  37. Size Issue - Work a round - Api Impl //

    commonMain - AstrosApi.kt @Serializable @JsExport data class AstrosApiResponse(val number: Int, val people: Array<People>, 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<AstrosApiResponse>(it) } } }
  38. Size Issue - Work a round - Api Impl //

    jsMain @JsExport object AstrosApiWrapper { private val astrosApi = AstrosApi(JsHttpClient()) fun getAstros(): Promise<AstrosApiResponse?> = GlobalScope.promise { astrosApi.getAstros() } }
  39. 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
  40. Thank you Droidcon NYC Jig a r Br a hmbh

    a tt 
 @sh a ktim a n_droid (Kotlin Sl a ck & Twitter)