Building Progressive Web Apps in Kotlin

2307a37297162f815342545a2068b2f1?s=47 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.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

December 05, 2019
Tweet

Transcript

  1. 3.
  2. 4.
  3. 5.
  4. 6.
  5. 9.
  6. 10.

    import { LitElement, html, property, customElement } from 'lit-element'; @customElement('simple-greeting')

    export class SimpleGreeting extends LitElement { @property() name = 'World'; render() { return html`<p>Hello, ${this.name}!*/p>`; } }
  7. 11.

    class SimpleGreeting : LitElement() { private var name: String =

    "World" override fun render(): dynamic { return "<p>Hello, $name!*/p>" } companion object { val properties = json("name" to String*:class) } }
  8. 13.

    TypeScript can also be weird! :) function typeScriptExample(wantNumber: boolean): number

    | string { if (wantNumber) { return 42 } else { return "Here is some text" } }
  9. 15.
  10. 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")) } }
  11. 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!}") }) }
  12. 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);
  13. 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);
  14. 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" }
  15. 24.
  16. 25.
  17. 27.

    Service Worker - index.html <!DOCTYPE html> <html lang="en"> <head> <meta

    charset="UTF-8"> <title>Kotlin/JS PWA Demo*/title> */head> <body> <div id="appContent">*/div> */body> <script src="main.js">*/script> */html>
  18. 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) }); }
  19. 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')); } });
  20. 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") } }) }
  21. 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
  22. 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) } })
  23. 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") }
  24. 37.

    Kitten API response { "count": 1, "kittens": [ { "name":

    "Lucy", "age": 3, "gender": "male", "color": "gray", "race": "siberian", "photoUri": "https:*/kitten.io/images/lucy.png" } ] }
  25. 38.

    Kotlin data classes @Serializable data class KittensResponse( val count: Int,

    val kittens: List<Kitten> ) @Serializable data class Kitten( val name: String, val age: Int, val gender: Gender, val color: Color, val race: Race, val photoUri: String )
  26. 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) }
  27. 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<KittensResponse>(url) } }
  28. 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}`) } }
  29. 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") } }
  30. 44.

    Promises.kt public suspend fun <T> Promise<T>.await(): T = suspendCancellableCoroutine {

    cont: CancellableContinuation<T> -> this@await.then( onFulfilled = { cont.resume(it) }, onRejected = { cont.resumeWithException(it) }) }
  31. 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") } } }
  32. 48.
  33. 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 } } } } }
  34. 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() } }
  35. 55.
  36. 56.

    kotlin { target { nodejs() browser() } sourceSets["main"].dependencies { implementation(kotlin("stdlib-js"))

    implementation(npm("jszip","3.2.2")) } } NPM dependencies in Gradle?!?
  37. 57.

    Declare the API in Kotlin external class ZipObject { fun

    async(type: String): Promise<Any?> } external class JSZip { fun file(name: String): Promise<ZipObject> fun loadAsync(data: ArrayBuffer): Promise<JSZip> }
  38. 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<Blob> } .then { val objectUrl = URL.createObjectURL(it) val img = document.querySelector("#kittenImage") img as HTMLImageElement img.src = objectUrl } }
  39. 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 }
  40. 60.
  41. 61.

    Impossible to convert to Kotlin? function typeScriptExample(wantNumber: boolean): number |

    string { if (wantNumber) { return 42 } else { return "Here is some text" } }
  42. 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() }
  43. 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;
  44. 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
  45. 70.

    Conclusions • JavaScript output can be very big • Kotlin

    wrappers needed • Undocumented build system • Missing code splitting (for Service Workers etc.) • Looks promising!
  46. 72.