Copenhagen Denmark
BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman
@ErikHellman
BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman @ErikHellman - - PowerPoint PPT Presentation
BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman @ErikHellman Copenhagen Denmark Actual Cross Platform! Types - Theyre pretty great! Type definitions for JavaScript DOM APIs lib.dom.d.ts import { LitElement, html, property,
Copenhagen Denmark
@ErikHellman
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"
return "<p>Hello, $name!*/p>" } companion object { val properties = json("name" to String*:class) } }
function javaScriptIsWeird(wantNumber) { if (wantNumber) { return 42 } else { return "Here is some text" } }
function typeScriptExample(wantNumber: boolean): number | string { if (wantNumber) { return 42 } else { return "Here is some text" } }
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")) } }
import kotlin.browser.window val document = window.document fun main() { val button = document.querySelector("#button") *: return button.addEventListener("click", { console.log("Clicked on button!}") }) }
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);
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);
https://developers.google.com/web/progressive-web-apps
manifest.json
Web App Manifest Service Worker Web UI
{ "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" }
index.html main.js service-worker.js
<!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>
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) }); }
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')); } });
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") } }) }
Input Output
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") } }) }
How can we create this file?
2 copies of Kotlin/JS stdlib!!!
import 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!") }) } }
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!") }) } }
Reference to Service Worker scope
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
self.addEventListener("fetch", { event -> event as FetchEvent self.caches.match(event.request) .then { it as Response? return@then it *: self.fetch(event.request) } })
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") }
{ "count": 1, "kittens": [ { "name": "Lucy", "age": 3, "gender": "male", "color": "gray", "race": "siberian", "photoUri": "https:*/kitten.io/images/lucy.png" } ] }
@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 )
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) }
class KittenApi { private val client = HttpClient(Js) { install(JsonFeature) } suspend fun fetchKittens(): KittensResponse { val url = "http:*/localhost:8080/kittens" return client.get<KittensResponse>(url) } }
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}`) } }
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") } }
public suspend fun <T> Promise<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> -> this@await.then(
}
implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.6.12")
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") } } }
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
+kitten */ Add text to LI element } } } } }
implementation(npm("@jetbrains/kotlin-react", "16.9.0-pre.83")) implementation(npm("@jetbrains/kotlin-react-dom", "16.9.0-pre.83"))
fun RBuilder.hello(name: String) { h1 { +"Hello, $name!" } } fun RBuilder.app() { hello("Erik") } fun main() { val element = document.querySelector("#app") *: return render(element) { app() } }
https://github.com/JetBrains/create-react-kotlin-app
$ npm install -g create-react-kotlin-app $ npx create-react-kotlin-app kotlin-create-react-demo
kotlin { target { nodejs() browser() } sourceSets["main"].dependencies { implementation(kotlin("stdlib-js")) implementation(npm("jszip","3.2.2")) } }
external class ZipObject { fun async(type: String): Promise<Any?> } external class JSZip { fun file(name: String): Promise<ZipObject> fun loadAsync(data: ArrayBuffer): Promise<JSZip> }
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 } }
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 }
function typeScriptExample(wantNumber: boolean): number | string { if (wantNumber) { return 42 } else { return "Here is some text" } }
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() }
E x p e r i m e n t a l ! ! !
kotlin.js.experimental.generateKotlinExternals=true
$ ./gradlew generateExternals
*/ 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;
@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
#KotlinConf
Erik Hellman @ErikHellman