Share
Embed
Intro to Web Dev with Kotlin
With front-end web apps becoming larger and more complex, and Node.js allowing Javascript to be used on the server side, the language has had a rapid rise in popularity. But for many JVM/Android developers, the thought of getting into front end web development and the Javascript ecosystem can seem quite unpleasant. However with Kotlin's ability to target JS, we can leverage the language features we've come to love, to make the experience a bit more fun. In this talk, we'll explore how we can build a Kotlin front-end application and even use Kotlin to interact with Node.js on the server.
Zachary Smith
February 13, 2019
Transcript
61 slides
-
allprojects { buildscript { ext.kotlin_version = '1.3.21' ext.kotlinx_html_version = "0.6.12"
repositories { mavenCentral() maven { url "https://dl.bintray.com/kotlin/kotlin-eap" } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-frontend-plugin:0.0.44" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } } Root - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' repositories { jcenter() }
dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } compileKotlin2Js { kotlinOptions.moduleKind = 'commonjs' kotlinOptions.outputFile = "$buildDir/js/index.js" } Backend - build.gradle -
{ "name": "backend", "version": "1.0.0", "description": "", "main": "index.js", "scripts":
{ "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./build/js/index.js" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.16.4", "kotlin": "^1.3.21", "request": "^2.88.0" } } Backend - package.json -
{ "name": "backend", "version": "1.0.0", "description": "", "main": "index.js", "scripts":
{ "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./build/js/index.js" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.16.4", "kotlin": "^1.3.21", "request": "^2.88.0" } } Backend - package.json -
external fun require(module: String): dynamic fun main() { val express
= require("express") val request = require("request") val app = express() ... } -
external fun require(module: String): dynamic fun main() { val express
= require("express") val request = require("request") val app = express() ... } Declare externally defined API returning dynamic type. -
Dynamic • The dynamic type basically turns off Kotlin's type
checker. • A dynamic value can be assigned to any variable or passed anywhere as a parameter. • Any value can be assigned to dynamic variable • Any value can be passed to a function that takes dynamic as a parameter. • Null-checks are disabled for dynamic values. -
external fun require(module: String): dynamic fun main() { ... app.use(express.static("public"))
app.get("/") { req, res -> res.send("public") } app.get("/todos") { req, res -> request.get("https://jsonplaceholder.typicode.com/todos") { err, response, body -> res.type("text/json") res.send(response.body) } } app.listen(3000) { println("Listening on port 3000") } } -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
apply plugin: 'kotlin2js' apply plugin: 'kotlin-dce-js' apply plugin: 'org.jetbrains.kotlin.frontend' apply
plugin: 'kotlinx-serialization' repositories { jcenter() maven { url "http://dl.bintray.com/kotlin/kotlin-dev" } maven { url "http://dl.bintray.com/kotlinx/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "http://dl.bintray.com/kotlin/kotlin-js-wrappers" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-html-js:0.6.12" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.10.0" implementation 'org.jetbrains:kotlin-react:16.6.0-pre.68-kotlin-1.3.20' implementation "org.jetbrains:kotlin-react-dom:16.6.0-pre.68-kotlin-1.3.20" } Frontend - build.gradle -
kotlinFrontend { downloadNodeJsVersion = 'latest' sourceMaps = true npm {
dependency "style-loader" dependency "react" dependency "react-dom" devDependency "karma" } webpackBundle { bundleName = "main" contentPath = file('src/main/web') } } Frontend - build.gradle -
compileKotlin2Js { kotlinOptions { metaInfo = true outputFile = "$project.buildDir.path/js/${project.name}.js"
sourceMap = true moduleKind = 'commonjs' main = "call" } } Frontend - build.gradle -
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> <title>Kotlin
React Demo</title> </head> <body> <div id="root"></div> <script type="text/javascript" src="main.bundle.js"></script> </body> </html> Index.html -
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> <title>Kotlin
React Demo</title> </head> <body> <div id="root"></div> <script type="text/javascript" src="main.bundle.js"></script> </body> </html> Index.html -
React • A JavaScript library for building user interfaces •
Uses the concept of Components • Components can manage their own state, and can be composed make complex UIs. -
Components • Components have a “render()” method. • Method takes
input data and returns “descriptions” of what to display. • “Descriptions” are defined in objects call React Elements -
React Elements • React Elements are cheap to create and
maintain. • React uses these to model the state of the UI. • When component state changes, render is called, and new React Elements are produced that reflect updated state. • Can diff this ui model with current DOM state and will only update the DOM nodes necessary to bring DOM to correct state -
fun render(container: Element?, callback: () -> Unit = {}, handler:
RBuilder.() -> Unit) = render(buildElements(handler), container, callback) -
fun render(container: Element?, callback: () -> Unit = {}, handler:
RBuilder.() -> Unit) = render(buildElements(handler), container, callback) -
fun render(container: Element?, callback: () -> Unit = {}, handler:
RBuilder.() -> Unit) = render(buildElements(handler), container, callback) -
fun render(container: Element?, callback: () -> Unit = {}, handler:
RBuilder.() -> Unit) = render(buildElements(handler), container, callback) Invokes external render function in React JS library. -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} Render method for defining UI -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} -
package react import kotlinext.js.* // Props external interface RProps val
RProps.children: Any get() = asDynamic().children var RProps.key: String get() = error("key cannot be read from props") set(value) { asDynamic().key = value } -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} -
inline fun RBuilder.div(..., block: RDOMBuilder<DIV>.() -> Unit): ReactElement: { return
tag(block) { ... } } fun <P:RProps, C:Component<P, *>> child( klazz: KClass<C>, handler: RHandler<P> ): ReactElement { val rClass = klazz.js as RClass<P> return rClass(handler) } -
class AppComponent : RComponent<RProps, RState>() { override fun RBuilder.render() {
div("container") { div("row") { h2("display-1") { +"Welcome to React with Kotlin" } } todoListComponent() } } } fun RBuilder.appComponent() = child(AppComponent::class) {} -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { override fun TodoListState.init(props:
TodoListProps) {...} override fun RBuilder.render() {...} private fun addTodo() {...} private fun deleteTodo(selectedTodo: TodoItem) {...} private fun fetchTodosFromNetwork() {...} } fun RBuilder.todoListComponent(items: List<TodoItem> = listOf()) { return child(TodoListComponent::class) { attrs.initialItems = items } } -
interface TodoListProps : RProps { var initialItems: List<TodoItem> } interface
TodoListState : RState { var todos: List<TodoItem> var text: String } -
@Serializable data class TodoItem( val userId: Int, val id: Int,
val title: String, val completed: Boolean ) -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { override fun TodoListState.init(props:
TodoListProps) { ... } override fun RBuilder.render() { div("row justify-content-center") { div("input-group mb-3") { input(type = InputType.text, name = "itemText", classes = "form-control") { key = "itemText" attrs { value = state.text placeholder = "Add a to-do item" onChangeFunction = { val target = it.target as HTMLInputElement setState { text = target.value } } } } ... } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { override fun TodoListState.init(props:
TodoListProps) { ... } override fun RBuilder.render() { div("row justify-content-center") { div("input-group mb-3") { input(type = InputType.text, name = "itemText", classes = "form-control") { key = "itemText" attrs { value = state.text placeholder = "Add a to-do item" onChangeFunction = { val target = it.target as HTMLInputElement setState { text = target.value } } } } ... } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { div("input-group mb-3") { ... div("input-group-append") { primaryButton("Add", ::addTodo) primaryButton("Get Todos From Network", ::fetchTodosFromNetwork) } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { div("input-group mb-3") { ... div("input-group-append") { primaryButton("Add", ::addTodo) primaryButton("Get Todos From Network", ::fetchTodosFromNetwork) } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { div("input-group mb-3") { ... div("input-group-append") { primaryButton("Add", ::addTodo) primaryButton("Get Todos From Network", ::fetchTodosFromNetwork) } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { ... } div("row justify-content-center") { ul("list-group") { state.todos.forEachIndexed { index, todo -> li("list-group-item d-flex justify-content-between align-items-center") { key = index.toString() +todo.title deleteButton("DELETE") { deleteTodo(todo) } } } } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { ... } div("row justify-content-center") { ul("list-group") { state.todos.forEachIndexed { index, todo -> li("list-group-item d-flex justify-content-between align-items-center") { key = index.toString() +todo.title deleteButton("DELETE") { deleteTodo(todo) } } } } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... override fun
RBuilder.render() { div("row justify-content-center") { ... } div("row justify-content-center") { ul("list-group") { state.todos.forEachIndexed { index, todo -> li("list-group-item d-flex justify-content-between align-items-center") { key = index.toString() +todo.title deleteButton("DELETE") { deleteTodo(todo) } } } } } } ... -
fun RDOMBuilder<*>.primaryButton(btnText: String, onClick: () -> Unit): ReactElement { return
button(classes = "btn btn-outline-secondary") { +btnText attrs { onClickFunction = { onClick() } } } } fun RDOMBuilder<*>.deleteButton(btnText: String, onClick: () -> Unit): ReactElement { return button(classes = "badge badge-danger badge-pill") { +btnText attrs { onClickFunction = { onClick() } } } } -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... private fun
addTodo() { if (state.text.isNotEmpty()) { setState { todos += TodoItem( userId = 1, id = generateId(), title = text, completed = false ) text = "" } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... private fun
deleteTodo(selectedTodo: TodoItem) { setState { todos = todos.filter { todoItem -> todoItem.id != selectedTodo.id } } } ... -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { ... private fun
fetchTodosFromNetwork() { xhrGet("https://jsonplaceholder.typicode.com/todos") { response -> setState { todos += Json.parse(TodoItem.serializer().list, response) } } } } -
fun xhrGet(url: String, callback: (String) -> Unit) { val request
= XMLHttpRequest() request.open("GET", url) request.onload = { if (request.readyState == 4.toShort() && request.status == 200.toShort()) { callback.invoke(request.responseText) } } request.send() } -
class TodoListComponent(props: TodoListProps) : RComponent<TodoListProps, TodoListState>(props) { override fun TodoListState.init(props:
TodoListProps) {...} override fun RBuilder.render() {...} private fun addTodo() {...} private fun deleteTodo(selectedTodo: TodoItem) {...} private fun fetchTodosFromNetwork() {...} } -
Advantages Backend • Can interact with existing node systems. •
Compile kotlin programs to deploy on AWS systems running node. • Isomorphic JS -> Sharing code between frontend and backend • Kotlin Common -> Shared code with other platforms (iOS & Android) -
Advantages Frontend • Statically-typed front end web development • Isomorphic
Kotlin/JS -> Sharing code between frontend and backend • Existing React wrapper libraries facilitate reactive programming. • Kotlin's language support for DSL's and extension allow for rich, fluid ui composition -
Disadvantages • Consuming JavaScript returns “dynamic” types. • 3rd party
wrappers are still limited. • Integration with popular libs would require writing your own wrappers or forgoing type safety. • Debugging can be difficult. -
• Your first Node.js app with Kotlin ◦ https://medium.com/@Miqubel/your-first-node-js-app-with-kotlin-30e07baa0bf7 •
Kotlin and Javascript ◦ https://www.baeldung.com/kotlin-javascript • Web App With Kotlin.js: Getting Started ◦ https://www.raywenderlich.com/201669-web-app-with-kotlin-js-getting-started • Kotlin/JS configuration made simple ◦ https://blog.kotlin-academy.com/kotlin-js-configuration-made-simple-ef0e361 fcd4 • Kotlin wrappers for popular JavaScript libraries ◦ https://github.com/JetBrains/kotlin-wrappers • Create React apps using Kotlin with no build configuration ◦ https://github.com/JetBrains/create-react-kotlin-app • Kotlin Full-stack Application Example ◦ https://github.com/Kotlin/kotlin-fullstack-sample • KotlinConf 2017 - How to Build a React App in Kotlin by Dave Ford ◦ https://www.youtube.com/watch?v=FDOECr-sT6U