$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. View Slide

  3. 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!

    View Slide

  4. What is Kotlin/JS

    View Slide

  5. 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

    View Slide

  6. Set up Kotlin/JS

    View Slide

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

    View Slide

  8. Set up Kotlin/JS
    Most likely next error!

    View Slide

  9. Set up Kotlin/JS
    Most likely next error!
    //
    root
    - >
    build.gradle.kts


    tasks.register("clean", Delete
    ::
    class) {


    delete(rootProject.buildDir)


    }
    Remove this!

    View Slide

  10. 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)

    View Slide

  11. Set up Kotlin/JS
    sh
    a
    red/src

    View Slide

  12. Set up Kotlin/JS
    sh
    a
    red/src

    View Slide

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

    View Slide

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

    View Slide

  15. 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?

    View Slide

  16. 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?

    View Slide

  17. 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

    View Slide

  18. Set up Kotlin/JS
    Option-2 steps:


    • Add mobileMain sourceSet
    • Move entire commonMain to mobileMain
    • Now slowly add JS support for existing features 👶🪜

    View Slide

  19. 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

    View Slide

  20. Set up Kotlin/JS
    sh
    a
    red/src

    View Slide

  21. JS build output

    View Slide

  22. JS build output

    View Slide

  23. compiler
    LEGACY - not recommended
    JS build output

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. How to expose Kotlin to JS?

    View Slide

  31. //
    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?

    View Slide

  32. //
    commonMain


    @JsExport


    class Greeting {


    fun greeting(): String {


    return "Hello, ${Platform().name}!"


    }


    }
    How to expose Kotlin to JS?

    View Slide

  33. / /
    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?

    View Slide

  34. 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?

    View Slide

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

    View Slide

  36. @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)

    View Slide

  37. @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?

    View Slide

  38. 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)

    View Slide

  39. 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"


    }


    }


    View Slide

  40. 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"


    }


    }


    View Slide

  41. 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"


    }


    }

    View Slide

  42. 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;


    }


    }

    View Slide

  43. How to expose Kotlin to JS?

    View Slide

  44. What about third party
    dependencies?

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

  47. 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”

    View Slide

  48. third p
    a
    rty dependencies

    View Slide

  49. third p
    a
    rty dependencies

    View Slide

  50. third p
    a
    rty dependencies

    View Slide

  51. third p
    a
    rty dependencies

    View Slide

  52. third p
    a
    rty dependencies

    View Slide

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

    View Slide

  54. How to go about
    Multiplatform?

    View Slide

  55. 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

    View Slide

  56. But for JS, bundle size matters!

    View Slide

  57. 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 😍)

    View Slide

  58. 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


    }


    View Slide

  59. 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

    View Slide

  60. 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


    }


    View Slide

  61. 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) }


    }


    }


    View Slide

  62. Size Issue - Work
    a
    round - Api Impl
    //
    jsMain


    @JsExport


    object AstrosApiWrapper {


    private val astrosApi = AstrosApi(JsHttpClient())


    fun getAstros(): Promise = GlobalScope.promise {


    astrosApi.getAstros()


    }


    }

    View Slide

  63. 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

    View Slide

  64. Buzz
    (Kotlin Slack)

    View Slide

  65. View Slide

  66. Thank you


    Droidcon NYC
    Jig
    a
    r Br
    a
    hmbh
    a
    tt

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

    View Slide