BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman @ErikHellman - - PowerPoint PPT Presentation

building progressive web apps in kotlin erik hellman
SMART_READER_LITE
LIVE PREVIEW

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,


slide-1
SLIDE 1

Copenhagen Denmark

BUILDING PROGRESSIVE WEB APPS IN KOTLIN Erik Hellman

@ErikHellman

slide-2
SLIDE 2

Actual Cross Platform!

slide-3
SLIDE 3
slide-4
SLIDE 4
slide-5
SLIDE 5
slide-6
SLIDE 6
slide-7
SLIDE 7

Types - They’re pretty great!

slide-8
SLIDE 8

lib.dom.d.ts

Type definitions for JavaScript DOM APIs

slide-9
SLIDE 9
slide-10
SLIDE 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>`; } }

slide-11
SLIDE 11

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

  • verride fun render(): dynamic {

return "<p>Hello, $name!*/p>" } companion object { val properties = json("name" to String*:class) } }

slide-12
SLIDE 12

JavaScript can be weird...

function javaScriptIsWeird(wantNumber) { if (wantNumber) { return 42 } else { return "Here is some text" } }

slide-13
SLIDE 13

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

“It’s complicated…”

slide-15
SLIDE 15

Kotlin/JS

slide-16
SLIDE 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")) } }

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

slide-18
SLIDE 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);

slide-19
SLIDE 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);

slide-20
SLIDE 20

Progressive Web Apps

slide-21
SLIDE 21

Reliable - Fast - Engaging

https://developers.google.com/web/progressive-web-apps

slide-22
SLIDE 22

manifest.json

Web App Manifest Service Worker Web UI

slide-23
SLIDE 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" }

slide-24
SLIDE 24
slide-25
SLIDE 25
slide-26
SLIDE 26

Service Worker

index.html main.js service-worker.js

slide-27
SLIDE 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>

slide-28
SLIDE 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) }); }

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

slide-30
SLIDE 30

Service Worker

slide-31
SLIDE 31

Kotlin/JS - Service Workers

slide-32
SLIDE 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") } }) }

slide-33
SLIDE 33

Kotlin/JS Output

Input Output

slide-34
SLIDE 34

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

How can we create this file?

slide-35
SLIDE 35

First solution - 2 Gradle modules!

2 copies of Kotlin/JS stdlib!!!

slide-36
SLIDE 36

Second solution - use the same script!

slide-37
SLIDE 37

Kotlin/JS - Main.kt

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!

slide-38
SLIDE 38

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!

slide-39
SLIDE 39

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

slide-40
SLIDE 40

Implementing the Service Worker

slide-41
SLIDE 41

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-42
SLIDE 42

Kotlin/JS - Implementing offmine 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-43
SLIDE 43

Calling your HTTP API with Kotlin/JS

slide-44
SLIDE 44

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-45
SLIDE 45

Kitten API response

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

slide-46
SLIDE 46

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 )

slide-47
SLIDE 47

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-48
SLIDE 48

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

slide-49
SLIDE 49

Kotlin/JS & Coroutines

slide-50
SLIDE 50

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-51
SLIDE 51

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-52
SLIDE 52

Promises.kt

public suspend fun <T> Promise<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> -> this@await.then(

  • nFulfilled = { cont.resume(it) },
  • nRejected = { cont.resumeWithException(it) })

}

slide-53
SLIDE 53

Kotlin/JS UI

slide-54
SLIDE 54

kotlinx.html

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

slide-55
SLIDE 55

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-56
SLIDE 56
slide-57
SLIDE 57

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

  • nClickFunction = { } */ Click listener

+kitten */ Add text to LI element } } } } }

slide-58
SLIDE 58

React

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

slide-59
SLIDE 59

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-60
SLIDE 60

Create React Kotlin App

https://github.com/JetBrains/create-react-kotlin-app

slide-61
SLIDE 61

$ npm install -g create-react-kotlin-app $ npx create-react-kotlin-app kotlin-create-react-demo

Create a React/Kotlin app

slide-62
SLIDE 62

NPM packages

slide-63
SLIDE 63
slide-64
SLIDE 64

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

NPM dependencies in Gradle?!?

slide-65
SLIDE 65

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

slide-66
SLIDE 66

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

slide-67
SLIDE 67

...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-68
SLIDE 68

dynamic

slide-69
SLIDE 69

Impossible to convert to Kotlin?

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

slide-70
SLIDE 70

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-71
SLIDE 71

Dukat

E x p e r i m e n t a l ! ! !

slide-72
SLIDE 72

gradle.properties

kotlin.js.experimental.generateKotlinExternals=true

slide-73
SLIDE 73

Generate externals task

$ ./gradlew generateExternals

slide-74
SLIDE 74

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-75
SLIDE 75

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-76
SLIDE 76

Is Kotlin/JS ready for production use?

slide-77
SLIDE 77

“It depends…”

slide-78
SLIDE 78

Conclusions

  • JavaScript output can be very big
  • Kotlin wrappers needed
  • Undocumented build system
  • Missing code splitting (for Service Workers etc.)
  • Looks promising!
slide-79
SLIDE 79

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

slide-80
SLIDE 80
slide-81
SLIDE 81

#KotlinConf

THANK YOU AND REMEMBER TO VOTE

Erik Hellman @ErikHellman