BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman...BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik...
Transcript of BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman...BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik...
CopenhagenDenmark
BUILDING PROGRESSIVE WEB APPS IN KOTLINErik Hellman
@ErikHellman
Actual Cross Platform!
Types - They’re pretty great!
lib.dom.d.ts
Type definitions for JavaScript DOM APIs
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>`; }}
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) }}
JavaScript can be weird...function javaScriptIsWeird(wantNumber) { if (wantNumber) { return 42 } else { return "Here is some text" }}
TypeScript can also be weird! :)function typeScriptExample(wantNumber: boolean): number | string { if (wantNumber) { return 42 } else { return "Here is some text" }}
“It’s complicated…”
Kotlin/JS
Kotlin/JS - build.gradle.ktsplugins { 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")) }}
Kotlin/JS - Main.ktimport kotlin.browser.window
val document = window.document
fun main() { val button = document.querySelector("#button") *: return button.addEventListener("click", { console.log("Clicked on button!}") })}
Kotlin/JS - main.jsif (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);
Kotlin/JS - main.jsvar 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);
Progressive Web Apps
Reliable - Fast - Engaginghttps://developers.google.com/web/progressive-web-apps
manifest.json
Web App Manifest Service Worker
Web UI
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"}
Service Workerindex.html
main.js
service-worker.js
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>
Service Worker - main.jsif ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') .then(() *> { console.log('Service Worker registered!') }) .catch(error *> { console.error('Service Worker registration failed!', error) });}
Service Worker - service-worker.jsself.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')); }});
Service Worker
Kotlin/JS - Service Workers
Kotlin/JS - Main.ktimport 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") } })}
Kotlin/JS Output
Input
Output
Kotlin/JS - Main.ktimport 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") } })}
How can we create this file?
First solution - 2 Gradle modules!
2 copies of Kotlin/JS stdlib!!!
Second solution - use the same script!
Kotlin/JS - Main.ktimport kotlin.browser.window
fun main() { window.addEventListener("load", { window.navigator.serviceWorker .register("/kotlin-js-pwa.js") .then { console.log("Service worker registered!") } .catch { console.error("Service Worker registration failed: $it") } })}
Same script as we’re currently running in!
external val self: ServiceWorkerGlobalScope
fun main() { try { window.addEventListener("load", { window.navigator.serviceWorker.register("/kotlin-js-pwa.js") }) } catch (t: Throwable) { self.addEventListener("install", { event -> console.log("Service Worker installed!") }) self.addEventListener("activate", { event -> console.log("Service Worker is now active!") }) }}
Kotlin/JS - Main.kt Throws ReferenceError in a Service Worker!
external val self: ServiceWorkerGlobalScope
fun main() { try { window.addEventListener("load", { window.navigator.serviceWorker.register("/kotlin-js-pwa.js") }) } catch (t: Throwable) { self.addEventListener("install", { event -> console.log("Service Worker installed!") }) self.addEventListener("activate", { event -> console.log("Service Worker is now active!") }) }}
Kotlin/JS - Main.kt Reference to Service Worker scope
Implementing the Service Worker
Kotlin/JS - Installing Service Workerconst 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
Kotlin/JS - Implementing offline cacheself.addEventListener("fetch", { event -> event as FetchEvent self.caches.match(event.request) .then { it as Response? return@then it *: self.fetch(event.request) }})
Calling your HTTP API with Kotlin/JS
Kotlinx.serialization + ktor clientplugins { 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")}
Kitten API response{ "count": 1, "kittens": [ { "name": "Lucy", "age": 3, "gender": "male", "color": "gray", "race": "siberian", "photoUri": "https:*/kitten.io/images/lucy.png" } ]}
Kotlin data classes@Serializabledata class KittensResponse( val count: Int, val kittens: List<Kitten>)
@Serializabledata class Kitten( val name: String, val age: Int, val gender: Gender, val color: Color, val race: Race, val photoUri: String)
kotlinx.serializationfun 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)}
Ktor client + kotlinx.serializationclass KittenApi { private val client = HttpClient(Js) { install(JsonFeature) }
suspend fun fetchKittens(): KittensResponse { val url = "http:*/localhost:8080/kittens" return client.get<KittensResponse>(url) }}
Kotlin/JS & Coroutines
JavaScript - async/awaitasync function registerServiceWorker() { try { await navigator.serviceWorker .register('/service-worker.js') console.log('Service worker registered!') } catch (e) { console.error(`Error registering service worker: ${e}`) }}
Kotlin/JS - Coroutinessuspend 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") }}
Promises.ktpublic suspend fun <T> Promise<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> -> [email protected]( onFulfilled = { cont.resume(it) }, onRejected = { cont.resumeWithException(it) })}
Kotlin/JS UI
kotlinx.htmlimplementation("org.jetbrains.kotlinx:kotlinx-html-js:0.6.12")
kotlinx.htmlfun 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") } }}
kotlinx.htmlfun 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 } } } }}
Reactimplementation(npm("@jetbrains/kotlin-react", "16.9.0-pre.83"))implementation(npm("@jetbrains/kotlin-react-dom", "16.9.0-pre.83"))
Reactfun RBuilder.hello(name: String) { h1 { +"Hello, $name!" }}
fun RBuilder.app() { hello("Erik")}
fun main() { val element = document.querySelector("#app") *: return render(element) { app() }}
Create React Kotlin Apphttps://github.com/JetBrains/create-react-kotlin-app
$ npm install -g create-react-kotlin-app$ npx create-react-kotlin-app kotlin-create-react-demo
Create a React/Kotlin app
NPM packages
kotlin { target { nodejs() browser() }
sourceSets["main"].dependencies { implementation(kotlin("stdlib-js")) implementation(npm("jszip","3.2.2")) }}
NPM dependencies in Gradle?!?
Declare the API in Kotlinexternal class ZipObject { fun async(type: String): Promise<Any?>}
external class JSZip { fun file(name: String): Promise<ZipObject> fun loadAsync(data: ArrayBuffer): Promise<JSZip>}
Use the JavaScript library in Kotlinfun 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 }}
...using coroutinessuspend 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}
dynamic
Impossible to convert to Kotlin?function typeScriptExample(wantNumber: boolean): number | string { if (wantNumber) { return 42 } else { return "Here is some 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()}
Dukat
Experimental!!!
gradle.propertieskotlin.js.experimental.generateKotlinExternals=true
Generate externals task$ ./gradlew generateExternals
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;
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
Is Kotlin/JS ready for production use?
“It depends…”
Conclusions● JavaScript output can be very big
● Kotlin wrappers needed
● Undocumented build system
● Missing code splitting (for Service Workers etc.)
● Looks promising!
The state of Kotlin/JS - 13:00 Today!
#KotlinConf
THANK YOUAND REMEMBER TO VOTE
Erik Hellman @ErikHellman