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. Copenhagen Denmark BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman

    @ErikHellman
  2. Actual Cross Platform!

  3. None
  4. None
  5. None
  6. None
  7. Types - They’re pretty great!

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

  9. None
  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>`; } }
  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) } }
  12. JavaScript can be weird... function javaScriptIsWeird(wantNumber) { if (wantNumber) {

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

    | string { if (wantNumber) { return 42 } else { return "Here is some text" } }
  14. “It’s complicated…”

  15. Kotlin/JS

  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")) } }
  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!}") }) }
  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);
  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);
  20. Progressive Web Apps

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

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

  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" }
  24. None
  25. None
  26. Service Worker index.html main.js service-worker.js

  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>
  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) }); }
  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')); } });
  30. Service Worker

  31. Kotlin/JS - Service Workers

  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") } }) }
  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
  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) } })
  35. Calling your HTTP API with Kotlin/JS

  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") }
  37. Kitten API response { "count": 1, "kittens": [ { "name":

    "Lucy", "age": 3, "gender": "male", "color": "gray", "race": "siberian", "photoUri": "https:*/kitten.io/images/lucy.png" } ] }
  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 )
  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) }
  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) } }
  41. Kotlin/JS & Coroutines

  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}`) } }
  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") } }
  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) }) }
  45. Kotlin/JS UI

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

  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") } } }
  48. None
  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 } } } } }
  50. React implementation(npm("@jetbrains/kotlin-react", "16.9.0-pre.83")) implementation(npm("@jetbrains/kotlin-react-dom", "16.9.0-pre.83"))

  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() } }
  52. Create React Kotlin App https://github.com/JetBrains/create-react-kotlin-app

  53. $ npm install -g create-react-kotlin-app $ npx create-react-kotlin-app kotlin-create-react-demo Create

    a React/Kotlin app
  54. NPM packages

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

    implementation(npm("jszip","3.2.2")) } } NPM dependencies in Gradle?!?
  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> }
  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 } }
  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 }
  60. dynamic

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

    string { if (wantNumber) { return 42 } else { return "Here is some text" } }
  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() }
  63. Dukat Experimental!!!

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

  65. Generate externals task $ ./gradlew generateExternals

  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;
  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
  68. Is Kotlin/JS ready for production use?

  69. “It depends…”

  70. Conclusions • JavaScript output can be very big • Kotlin

    wrappers needed • Undocumented build system • Missing code splitting (for Service Workers etc.) • Looks promising!
  71. The state of Kotlin/JS - 13:00 Today!

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