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

Building Progressive Web Apps in Kotlin

Erik Hellman
December 05, 2019

Building Progressive Web Apps in Kotlin

Building modern web application, also known as Progressive Web Apps (PWA), depends a lot on tooling and languages built for and around JavaScript, and in many cases TypeScript. But what if you want to use Kotlin instead?

In this session, we will introduce how to use Kotlin and its support for JavaScript to Progressive Web Apps. You'll learn about Kotlin/JS, as well as how to use it with frameworks like React, Vue.js or LitElement.

Erik Hellman

December 05, 2019
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. Copenhagen
    Denmark
    BUILDING PROGRESSIVE
    WEB APPS IN KOTLIN
    Erik Hellman
    @ErikHellman

    View Slide

  2. Actual Cross Platform!

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. Types - They’re pretty great!

    View Slide

  8. lib.dom.d.ts
    Type definitions for
    JavaScript DOM APIs

    View Slide

  9. View Slide

  10. import { LitElement, html, property, customElement } from 'lit-element';
    @customElement('simple-greeting')
    export class SimpleGreeting extends LitElement {
    @property() name = 'World';
    render() {
    return html`Hello, ${this.name}!*/p>`;
    }
    }

    View Slide

  11. class SimpleGreeting : LitElement() {
    private var name: String = "World"
    override fun render(): dynamic {
    return "Hello, $name!*/p>"
    }
    companion object {
    val properties = json("name" to String*:class)
    }
    }

    View Slide

  12. JavaScript can be weird...
    function javaScriptIsWeird(wantNumber) {
    if (wantNumber) {
    return 42
    } else {
    return "Here is some text"
    }
    }

    View Slide

  13. TypeScript can also be weird! :)
    function typeScriptExample(wantNumber: boolean): number | string {
    if (wantNumber) {
    return 42
    } else {
    return "Here is some text"
    }
    }

    View Slide

  14. “It’s complicated…”

    View Slide

  15. Kotlin/JS

    View Slide

  16. Kotlin/JS - build.gradle.kts
    plugins { id("org.jetbrains.kotlin.js") version "1.3.61" }
    group = "se.hellsoft"
    version = "1.0-SNAPSHOT"
    repositories {
    mavenCentral()
    jcenter()
    }
    kotlin {
    target {
    nodejs()
    browser()
    }
    sourceSets["main"].dependencies {
    implementation(kotlin("stdlib-js"))
    }
    }

    View Slide

  17. Kotlin/JS - Main.kt
    import kotlin.browser.window
    val document = window.document
    fun main() {
    val button = document.querySelector("#button") *: return
    button.addEventListener("click", {
    console.log("Clicked on button!}")
    })
    }

    View Slide

  18. Kotlin/JS - main.js
    if (typeof kotlin **= 'undefined') {
    throw new Error("Error loading module 'test'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'test'.");
    }
    var test = function (_, Kotlin) {
    'use strict';
    var Unit = Kotlin.kotlin.Unit;
    var document;
    function main$lambda(it) {
    console.log('Clicked on button!}');
    return Unit;
    }
    function main() {
    var tmp$;
    tmp$ = document.querySelector('#button');
    if (tmp$ *= null) {
    return;
    }
    var button = tmp$;
    button.addEventListener('click', main$lambda);
    }
    Object.defineProperty(_, 'document', {
    get: function () {
    return document;
    }
    });
    _.main = main;
    document = window.document;
    main();
    Kotlin.defineModule('test', _);
    return _;
    }(typeof test **= 'undefined' ? {} : test, kotlin);

    View Slide

  19. Kotlin/JS - main.js
    var main = function (_, Kotlin) {
    **.
    function main$lambda(it) {
    console.log('Clicked on button!}');
    return Unit;
    }
    function main() {
    var tmp$;
    tmp$ = document.querySelector('#button');
    if (tmp$ *= null) {
    return;
    }
    var button = tmp$;
    button.addEventListener('click', main$lambda);
    }
    **.
    main();
    **.
    }(typeof main **= 'undefined' ? {} : main, kotlin);

    View Slide

  20. Progressive Web Apps

    View Slide

  21. Reliable - Fast - Engaging
    https://developers.google.com/web/progressive-web-apps

    View Slide

  22. manifest.json
    Web App Manifest Service Worker
    Web UI

    View Slide

  23. Web App Manifest - manifest.json
    {
    "short_name": "Maps",
    "name": "Google Maps",
    "icons": [
    {
    "src": "/images/icons-192.png",
    "type": "image/png",
    "sizes": "192x192"
    },
    {
    "src": "/images/icons-512.png",
    "type": "image/png",
    "sizes": "512x512"
    }
    ],
    "start_url": "/maps/?source=pwa",
    "background_color": "#3367D6",
    "display": "standalone",
    "scope": "/maps/",
    "theme_color": "#3367D6"
    }

    View Slide

  24. View Slide

  25. View Slide

  26. Service Worker
    index.html
    main.js
    service-worker.js

    View Slide

  27. Service Worker - index.html




    Kotlin/JS PWA Demo*/title>
    */head>

    */div>
    */body>
    */script><br/>*/html><br/>

    View Slide

  28. Service Worker - main.js
    if ('serviceWorker' in navigator) {
    navigator.serviceWorker
    .register('/service-worker.js')
    .then(() *> {
    console.log('Service Worker registered!')
    })
    .catch(error *> {
    console.error('Service Worker registration failed!', error)
    });
    }

    View Slide

  29. Service Worker - service-worker.js
    self.addEventListener('install', event *> {
    console.log('Service Worker installed!')
    });
    self.addEventListener('activate', event *> {
    console.log('Service Worker is now active!')
    });
    self.addEventListener('fetch', event *> {
    const url = new URL(event.request.url);
    if (url.origin **= location.origin *& url.pathname **= '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
    }
    });

    View Slide

  30. Service Worker

    View Slide

  31. Kotlin/JS - Service Workers

    View Slide

  32. Kotlin/JS - Main.kt
    import kotlin.browser.window
    fun main() {
    window.addEventListener("load", {
    window.navigator.serviceWorker
    .register("/service-worker.js")
    .then { console.log("Service worker registered!") }
    .catch { console.error("Service Worker registration failed: $it") }
    })
    }

    View Slide

  33. Kotlin/JS - Installing Service Worker
    const val CACHE_NAME = "my-site-cache-v1"
    val urlsToCache = arrayOf(
    "/",
    "/styles/main.css",
    "/images/dog.svg",
    "/images/cat.cvg"
    )
    external val self: ServiceWorkerGlobalScope
    fun installServiceWorker() {
    self.addEventListener("install", { event ->
    event as InstallEvent
    event.waitUntil(
    self.caches.open(CACHE_NAME)
    .then { it.addAll(urlsToCache) }
    )
    }
    }
    Reference to Service Worker scope

    View Slide

  34. Kotlin/JS - Implementing offline cache
    self.addEventListener("fetch", { event ->
    event as FetchEvent
    self.caches.match(event.request)
    .then {
    it as Response?
    return@then it *: self.fetch(event.request)
    }
    })

    View Slide

  35. Calling your HTTP API with Kotlin/JS

    View Slide

  36. Kotlinx.serialization + ktor client
    plugins {
    id("org.jetbrains.kotlin.js") version "1.3.61"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.3.61"
    }
    sourceSets["main"].dependencies {
    implementation(kotlin("stdlib-js"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.14.0")
    implementation("io.ktor:ktor-client-json-js:1.2.6")
    implementation("io.ktor:ktor-client-js:1.2.6")
    }

    View Slide

  37. Kitten API response
    {
    "count": 1,
    "kittens": [
    {
    "name": "Lucy",
    "age": 3,
    "gender": "male",
    "color": "gray",
    "race": "siberian",
    "photoUri": "https:*/kitten.io/images/lucy.png"
    }
    ]
    }

    View Slide

  38. Kotlin data classes
    @Serializable
    data class KittensResponse(
    val count: Int,
    val kittens: List
    )
    @Serializable
    data class Kitten(
    val name: String,
    val age: Int,
    val gender: Gender,
    val color: Color,
    val race: Race,
    val photoUri: String
    )

    View Slide

  39. kotlinx.serialization
    fun testSerialization(kittensResponse: KittensResponse): KittensResponse {
    val serializer = KittensResponse.serializer()
    val json = Json(JsonConfiguration.Stable)
    val jsonData = json.stringify(serializer, kittensResponse)
    println(jsonData)
    return json.parse(serializer, jsonData)
    }

    View Slide

  40. Ktor client + kotlinx.serialization
    class KittenApi {
    private val client = HttpClient(Js) {
    install(JsonFeature)
    }
    suspend fun fetchKittens(): KittensResponse {
    val url = "http:*/localhost:8080/kittens"
    return client.get(url)
    }
    }

    View Slide

  41. Kotlin/JS & Coroutines

    View Slide

  42. JavaScript - async/await
    async function registerServiceWorker() {
    try {
    await navigator.serviceWorker
    .register('/service-worker.js')
    console.log('Service worker registered!')
    } catch (e) {
    console.error(`Error registering service worker: ${e}`)
    }
    }

    View Slide

  43. Kotlin/JS - Coroutines
    suspend fun registerServiceWorker() {
    try {
    window.navigator.serviceWorker
    .register("/service-worker.js").await()
    console.log("Service Worker registered!")
    } catch (e: Exception) {
    console.error("Failed to register service worker: $e")
    }
    }

    View Slide

  44. Promises.kt
    public suspend fun Promise.await(): T
    = suspendCancellableCoroutine { cont: CancellableContinuation ->
    [email protected](
    onFulfilled = { cont.resume(it) },
    onRejected = { cont.resumeWithException(it) })
    }

    View Slide

  45. Kotlin/JS UI

    View Slide

  46. kotlinx.html
    implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.6.12")

    View Slide

  47. kotlinx.html
    fun main() {
    val appRoot = document.querySelector("#app") *: return
    appRoot.append {
    h1 { +"Hello, World!" }
    p {
    +"Unary plus operator appends String to tag."
    img(alt = "Photo of the cutest cat",
    src = "/cookie.jpg")
    }
    }
    }

    View Slide

  48. View Slide

  49. kotlinx.html
    fun main() {
    val kittens = listOf("Lucy", "Cookie", "Mittens", "Daisy", "Smokey")
    val appRoot = document.querySelector("#app") *: return
    appRoot.append {
    ul {
    for ((index, kitten) in kittens.withIndex()) {
    li {
    val color = if (index % 2 *= 0) "red" else "blue"
    classes = setOf(color) */ Set the CSS class
    onClickFunction = { } */ Click listener
    +kitten */ Add text to LI element
    }
    }
    }
    }
    }

    View Slide

  50. React
    implementation(npm("@jetbrains/kotlin-react", "16.9.0-pre.83"))
    implementation(npm("@jetbrains/kotlin-react-dom", "16.9.0-pre.83"))

    View Slide

  51. React
    fun RBuilder.hello(name: String) {
    h1 {
    +"Hello, $name!"
    }
    }
    fun RBuilder.app() {
    hello("Erik")
    }
    fun main() {
    val element = document.querySelector("#app") *: return
    render(element) {
    app()
    }
    }

    View Slide

  52. Create React Kotlin App
    https://github.com/JetBrains/create-react-kotlin-app

    View Slide

  53. $ npm install -g create-react-kotlin-app
    $ npx create-react-kotlin-app kotlin-create-react-demo
    Create a React/Kotlin app

    View Slide

  54. NPM packages

    View Slide

  55. View Slide

  56. kotlin {
    target {
    nodejs()
    browser()
    }
    sourceSets["main"].dependencies {
    implementation(kotlin("stdlib-js"))
    implementation(npm("jszip","3.2.2"))
    }
    }
    NPM dependencies in Gradle?!?

    View Slide

  57. Declare the API in Kotlin
    external class ZipObject {
    fun async(type: String): Promise
    }
    external class JSZip {
    fun file(name: String): Promise
    fun loadAsync(data: ArrayBuffer): Promise
    }

    View Slide

  58. Use the JavaScript library in Kotlin
    fun main() {
    val zip = JSZip()
    window.fetch("/kitten-photos.zip")
    .then { it.arrayBuffer() }
    .then { zip.loadAsync(it) }
    .then { it.file("lucy.jpg") }
    .then { it.async("blob") as Promise }
    .then {
    val objectUrl = URL.createObjectURL(it)
    val img = document.querySelector("#kittenImage")
    img as HTMLImageElement
    img.src = objectUrl
    }
    }

    View Slide

  59. ...using coroutines
    suspend fun loadImageFromZip(url:String) {
    val zip = JSZip()
    val response = window.fetch(url).await()
    val zipBuffer = response.arrayBuffer().await()
    val zipObject = zip.loadAsync(zipBuffer).await()
    val zipData = zipObject.file("lucy.jpg").await()
    val imageBlob = zipData.async("blob").await() as Blob
    val objectUrl = URL.createObjectURL(imageBlob)
    val img = document.querySelector("#kittenImage")
    img as HTMLImageElement
    img.src = objectUrl
    }

    View Slide

  60. dynamic

    View Slide

  61. Impossible to convert to Kotlin?
    function typeScriptExample(wantNumber: boolean): number | string {
    if (wantNumber) {
    return 42
    } else {
    return "Here is some text"
    }
    }

    View Slide

  62. dynamic to the rescue!
    fun testExternal() {
    val result: dynamic = typeScriptExample(false)
    val text = result as String
    console.log("Result is a string of length ${text.length}")
    result.can().call().anything.without().compile.error()
    }

    View Slide

  63. Dukat
    Experimental!!!

    View Slide

  64. gradle.properties
    kotlin.js.experimental.generateKotlinExternals=true

    View Slide

  65. Generate externals task
    $ ./gradlew generateExternals

    View Slide

  66. left-pad/index.d.ts
    */ Type definitions for left-pad 1.2.0
    */ Project: https:*/github.com/stevemao/left-pad
    */ Definitions by: Zlatko Andonovski, Andrew Yang, Chandler Fang and Zac
    Xu
    declare function leftPad(str: string|number, len: number, ch*:
    string|number): string;
    declare namespace leftPad { }
    export = leftPad;

    View Slide

  67. Generated externals: index.module_left-pad.kt
    @JsModule("left-pad")
    external fun leftPad(str: String, len: Number, ch: String? = definedExternally ** null
    */): String
    @JsModule("left-pad")
    external fun leftPad(str: String, len: Number, ch: Number? = definedExternally ** null
    */): String
    @JsModule("left-pad")
    external fun leftPad(str: Number, len: Number, ch: String? = definedExternally ** null
    */): String
    @JsModule("left-pad")
    external fun leftPad(str: Number, len: Number, ch: Number? = definedExternally ** null
    */): String
    @JsModule("left-pad")
    external fun leftPad(str: String, len: Number): String
    @JsModule("left-pad")
    external fun leftPad(str: Number, len: Number): String

    View Slide

  68. Is Kotlin/JS ready for production use?

    View Slide

  69. “It depends…”

    View Slide

  70. Conclusions

    JavaScript output can be very big

    Kotlin wrappers needed

    Undocumented build system

    Missing code splitting (for Service Workers etc.)

    Looks promising!

    View Slide

  71. The state of Kotlin/JS - 13:00 Today!

    View Slide

  72. View Slide

  73. #KotlinConf
    THANK YOU
    AND
    REMEMBER
    TO VOTE
    Erik Hellman @ErikHellman

    View Slide