Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Actual Cross Platform!

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Types - They’re pretty great!

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

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>`; } }

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

“It’s complicated…”

Slide 15

Slide 15 text

Kotlin/JS

Slide 16

Slide 16 text

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")) } }

Slide 17

Slide 17 text

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!}") }) }

Slide 18

Slide 18 text

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);

Slide 19

Slide 19 text

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);

Slide 20

Slide 20 text

Progressive Web Apps

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

manifest.json Web App Manifest Service Worker Web UI

Slide 23

Slide 23 text

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" }

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Service Worker - index.html Kotlin/JS PWA Demo*/title> */head> <body> <div id="appContent">*/div> */body> <script src="main.js">*/script> */html>

Slide 28

Slide 28 text

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) }); }

Slide 29

Slide 29 text

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')); } });

Slide 30

Slide 30 text

Service Worker

Slide 31

Slide 31 text

Kotlin/JS - Service Workers

Slide 32

Slide 32 text

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") } }) }

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Calling your HTTP API with Kotlin/JS

Slide 36

Slide 36 text

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") }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 )

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Kotlin/JS & Coroutines

Slide 42

Slide 42 text

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}`) } }

Slide 43

Slide 43 text

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") } }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Kotlin/JS UI

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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") } } }

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

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 } } } } }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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() } }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

NPM packages

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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 }

Slide 58

Slide 58 text

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 } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

dynamic

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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() }

Slide 63

Slide 63 text

Dukat Experimental!!!

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Generate externals task $ ./gradlew generateExternals

Slide 66

Slide 66 text

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;

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Is Kotlin/JS ready for production use?

Slide 69

Slide 69 text

“It depends…”

Slide 70

Slide 70 text

Conclusions ● JavaScript output can be very big ● Kotlin wrappers needed ● Undocumented build system ● Missing code splitting (for Service Workers etc.) ● Looks promising!

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

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