$30 off During Our Annual Pro Sale. View Details »

Full Stack Kotlin

Full Stack Kotlin

Guillermo Orellana

February 12, 2019
Tweet

More Decks by Guillermo Orellana

Other Decks in Programming

Transcript

  1. Full Stack Kotlin Guillermo Orellana @wiyarmir February 2019 Kotlin London

  2. None
  3. + =

  4. https://www.thinkgeek.com/product/e554/

  5. #10YearChallenge https://www.pinterest.co.uk/pin/225813368792712452/

  6. How to learn something?

  7. None
  8. Goals •100% Kotlin •??? •Profit https://medium.com/elementaryos/all-aboard-the-meson-future-hype-train-2b6c478b6b9e

  9. Keynotedex

  10. None
  11. Web

  12. KotlinJS + Kotlin Frontend Plugin

  13. KotlinJS

  14. dynamic https://www.joya.life/en/blog/the-power-of-kryptonite/

  15. val a: dynamic = 3 a.`types?` .I.`haven't`.heard .that.name.`in`.years = "!"

  16. None
  17. Kotlin Frontend Plugin

  18. None
  19. kotlinFrontend { downloadNodeJsVersion = "8.11.2" sourceMaps = false }

  20. npm { dependency "react", "16.3.1" dependency "react-dom" dependency "react-router" dependency

    "react-router-dom" devDependency "babel-loader" devDependency "babel-core" devDependency "css-loader" devDependency "style-loader" devDependency "source-map-loader" }
  21. webpackBundle { publicPath = "/frontend/" port = 8080 proxyUrl =

    "http: //localhost:9090/" sourceMapEnabled = true stats = "errors-only" }
  22. ./gradlew web:run > Task :web:nodejs-download UP-TO-DATE > Task :web:npm-preunpack UP-TO-DATE

    > Task :web:npm-configure > Task :web:npm-install UP-TO-DATE > Task :web:npm-index UP-TO-DATE > Task :web:npm-deps > Task :web:npm > Task :web:packages > Task :web:compileKotlin2Js > Task :web:compileJava NO-SOURCE > Task :web:processResources UP-TO-DATE > Task :web:classes > Task :web:compileTestKotlin2Js NO-SOURCE > Task :web:webpack-config UP-TO-DATE > Task :web:karma-config SKIPPED > Task :web:karma-start SKIPPED > Task :web:processTestResources NO-SOURCE > Task :web:runDceKotlinJs > Task :web:runDceTestKotlinJs NO-SOURCE > Task :web:webpack-run webpack started, see http: //localhost:8080/ > Task :web:run BUILD SUCCESSFUL in 9s 15 actionable tasks: 5 executed, 10 up-to-date ./gradlew web:run
  23. None
  24. React in Kotlin https://en.wikipedia.org/wiki/React_(JavaScript_library) +

  25. Components

  26. const Submission = (props) => ( <div className={"col-md-4"}> <h4>{props.submission.title} </h4>

    <p>{props.submission.abstract} </p> <Link to={`${props.submission.submissionUrl}`} className={"btn btn-secondary btn-lg"}> {"View details"} </Link> </div> ); // Usage <Submission submission={{…}} />
  27. Rendered component

  28. Asynchronous operations

  29. fun fetchUserProfileFromId(userId: String) = promise { val user = userProfile(userId)

    setState { userProfile = user } } .catch { console.error(it) }
  30. Backend

  31. None
  32. https://vertx.io/ https://spring.io/ http://ktor.io/

  33. Ktor http://ktor.io/

  34. "Easy to use, fun and asynchronous." http://ktor.io/

  35. fun main(args: Array<String>) { embeddedServer(Netty, 8080) { routing { get("/")

    { call.respondText("My Example Blog", ContentType.Text.Html) } } }.start(wait = true) } http://ktor.io/
  36. class ApplicationPage : Template<HTML> { val caption = Placeholder<TITLE>() val

    head = Placeholder<HEAD>() override fun HTML.apply() { head { meta { charset = "utf-8" } meta { name = "viewport" content = "width=device-width, initial-scale=1.0" } title { insert(caption) } insert(head) } body { div { id = "content" } script(src = "frontend/frontend.bundle.js") } } }
  37. class KeynotedexPageContent : Template<HTML> { val head = Placeholder<HEAD>() val

    bundle = Placeholder<SCRIPT>() override fun HTML.apply() { head { meta { charset = "utf-8" } meta { name = "viewport" content = "width=device-width, initial-scale=1.0, shrink-to-fit=no" } title { +"Keynotedex" } insert(head) link( rel = LinkRel.stylesheet, type = LinkType.textCss, href = "https: //maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" ) { attributes["integrity"] = "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" attributes["crossorigin"] = "anonymous" } } body { div { id = "content" } script(src = "https: //code.jquery.com/jquery-3.2.1.slim.min.js") { attributes["integrity"] = "sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" attributes["crossorigin"] = "anonymous" } script(src = "https: //cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js") { attributes["integrity"] = "sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" attributes["crossorigin"] = "anonymous" } script(src = "https: //maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js") { attributes["integrity"] = "sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" attributes["crossorigin"] = "anonymous" } script { insert(bundle) } } } }
  38. Don't overdose on builders

  39. fun Route.index() { static("frontend") { resource("web.bundle.js") } accept(ContentType.Text.Html) { get<IndexPage>

    { val model = mapOf( "jsbundle" to "/frontend/web.bundle.js" ) call.respond(FreeMarkerContent("index.ftl", model)) } } }
  40. API Tests

  41. @Test fun `when userId and password provided then user is

    created`() = testApp(mockStorage) { handleRequest(HttpMethod.Post, endpoint) { addHeader( HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString() ) setBody( listOf( "userId" to testId, "password" to testPassword ).formUrlEncode() ) }.apply { assertThat(response.status(), equalTo(HttpStatusCode.Created)) } }
  42. Shared code

  43. Kotlin Multiplatform common jvm android js native compile

  44. Platform specific code actual class Foo actual constructor(val bar: String)

    { actual fun frob() { println("Frobbing the $bar") } } Native Common expect class Foo(bar: String) { fun frob() }
  45. Platform specific module common common-jvm common-android common-js common-native expectedBy jvm

    android js native compile
  46. kotlinx.serialization

  47. @Serializable data class SubmissionResponse( val submission: Submission )

  48. val submissionResponse = JSON.parse( responseText ) call.respond( JSON.stringify( SubmissionResponse() )

    ) JSON Serialise Deserialise
  49. None
  50. val submissionResponse = JSON.parse( responseText ) Be careful with implicits

  51. val submissionResponse = JSON.parse( SubmissionResponse ::class.serializer(), responseText ) Be careful

    with implicits
  52. Reflection

  53. val submissionResponse = JSON.parse( SubmissionResponse.serializer(), responseText ) Explicit serialiser

  54. Deployment

  55. $ git push heroku master

  56. web: java \ -Dserver.port=$PORT \ $JAVA_OPTS \ -jar backend/build/libs /*-release.jar

  57. task stage() { group "distribution" dependsOn(':backend:release') }

  58. $ git push dokku master

  59. Counting objects: 155, done. Delta compression using up to 36

    threads. Compressing objects: 100% (50/50), done. Writing objects: 100% (89/89), 17.45 KiB | 0 bytes/s, done. Total 89 (delta 33), reused 75 (delta 19) remote: -----> Cleaning up ... remote: -----> Building keynotedex from herokuish ... remote: -----> Adding BUILD_ENV to build environment ... remote: -----> Gradle app detected remote: -----> Installing JDK 1.8 ... done remote: -----> Building Gradle app ... remote: -----> executing ./gradlew stage remote: To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: https: //docs.gradle.org/4.10.2/userguide/gradle_daemon.html. remote: Daemon will be stopped at the end of the build stopping after processing remote: > Task :buildSrc:discoverMainScriptsExtensions remote: > Task :buildSrc:compileKotlin remote: > Task :buildSrc:compileJava NO-SOURCE remote: > Task :buildSrc:compileGroovy NO-SOURCE remote: > Task :buildSrc:processResources NO-SOURCE remote: > Task :buildSrc:classes UP-TO-DATE remote: > Task :buildSrc:inspectClassesForKotlinIC remote: > Task :buildSrc:jar remote: > Task :buildSrc:assemble remote: > Task :buildSrc:discoverTestScriptsExtensions remote: > Task :buildSrc:compileTestKotlin NO-SOURCE remote: > Task :buildSrc:compileTestJava NO-SOURCE remote: > Task :buildSrc:compileTestGroovy NO-SOURCE remote: > Task :buildSrc:processTestResources NO-SOURCE remote: > Task :buildSrc:testClasses UP-TO-DATE remote: > Task :buildSrc:test NO-SOURCE remote: > Task :buildSrc:check UP-TO-DATE remote: > Task :buildSrc:build remote: > Task :backend:clean UP-TO-DATE remote: > Task :backend:discoverMainScriptsExtensions remote: > Task :common:compileJava NO-SOURCE remote: > Task :common:compileKotlinCommon remote: > Task :common:processResources NO-SOURCE remote: > Task :common:classes remote: > Task :common:inspectClassesForKotlinIC remote: > Task :common:jar remote: > Task :common-jvm:discoverMainScriptsExtensions remote: > Task :common-jvm:compileKotlin remote: > Task :common-jvm:compileJava NO-SOURCE remote: > Task :common-jvm:processResources NO-SOURCE remote: > Task :common-jvm:classes UP-TO-DATE remote: > Task :common-jvm:inspectClassesForKotlinIC remote: > Task :common-jvm:jar remote: > Task :backend:compileKotlin remote: remote: w: /tmp/build/backend/src/main/kotlin/es/guillermoorellana/keynotedex/backend/Application.kt: (52, 13): This declaration is experimental and its usage should be marked with '@io.ktor.locations.KtorExperimentalLocationsAPI' or '@UseExperimental(io.ktor.locations.KtorExperimentalLocationsAPI ::class)' remote: w: /tmp/build/backend/src/main/kotlin/es/guillermoorellana/keynotedex/backend/Application.kt: (85, 9): This declaration is experimental and its usage should be marked with '@io.ktor.locations.KtorExperimentalLocationsAPI' or '@UseExperimental(io.ktor.locations.KtorExperimentalLocationsAPI ::class)'
  60. remote: "id" : "Keynotedex", remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 13

    remote: "modules" : [ remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 13 remote: "es.guillermoorellana.keynotedex.backend.ApplicationKt.keynotedex" remote: ] remote: }, remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 2 remote: "deployment" : { remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 7 remote: "autoreload" : true, remote: # env var ENVIRONMENT remote: "environment" : "production", remote: # env var PORT remote: "port" : "5000", remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 8 remote: "watch" : [ remote: # application.conf @ jar:file:/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar!/application.conf: 8 remote: "keynotedex" remote: ] remote: }, remote: # Content hidden remote: "security" : " ***" remote: } remote: 2019-02-09 16:33:31.774 [main] DEBUG Keynotedex - Java Home: /app/.jdk remote: 2019-02-09 16:33:31.797 [main] DEBUG Keynotedex - Class Loader: sun.misc.Launcher$AppClassLoader@33909752: [/app/backend/build/libs/backend-0.0.1-SNAPSHOT-release.jar] remote: 2019-02-09 16:33:31.807 [main] INFO Keynotedex - No ktor.deployment.watch patterns match classpath entries, automatic reload is not active remote: 2019-02-09 16:33:34.270 [main] WARN Keynotedex - Populating db with mock data remote: 2019-02-09 16:33:35.991 [main] INFO Keynotedex - Responding at http: //0.0.0.0:5000 remote: 2019-02-09 16:33:35.993 [main] TRACE Keynotedex - Application started: io.ktor.application.Application@274872f8 remote: 2019-02-09 16:33:37.490 [nettyCallPool-4-1] TRACE Keynotedex - 200 OK: GET - / remote: 2019-02-09 16:33:37.576 [nettyCallPool-4-2] TRACE Keynotedex - 200 OK: GET - /logout remote: 2019-02-09 16:33:41.958 [nettyCallPool-4-3] TRACE Keynotedex - 200 OK: GET - /frontend/web.bundle.js remote: =====> end keynotedex web container output remote: -----> Running post-deploy remote: -----> Configuring keynotedex.wiyarmir.es ...(using built-in template) remote: -----> Creating https nginx.conf remote: -----> Running nginx-pre-reload remote: Reloading nginx remote: -----> Setting config vars remote: DOKKU_APP_RESTORE: 1 remote: -----> Found previous container(s) (5e0586566aa2) named keynotedex.web.1 remote: =====> Renaming container (5e0586566aa2) keynotedex.web.1 to keynotedex.web.1.1549730034 remote: =====> Renaming container (321dd6999ae1) cocky_ride to keynotedex.web.1 remote: -----> Attempting to run scripts.dokku.postdeploy from app.json (if defined) remote: -----> Shutting down old containers in 60 seconds remote: =====> 5e0586566aa204eb393e82496cb7d1a26d3f13471e81c95f559f01fb8788da22 remote: =====> Application deployed: remote: http: //keynotedex.wiyarmir.es remote: https: //keynotedex.wiyarmir.es remote: To dokku@dev.wiyarmir.es:keynotedex 8d2a091 ..abee039 master -> master
  61. $ git push origin master $ git push dokku master

  62. None
  63. deploy-backend: docker: - image: buildpack-deps:trusty steps: - checkout - run:

    name: Deploy Master to Heroku command: | ssh-keyscan -H dev.wiyarmir.es >> ~/.ssh/known_hosts git push $HEROKU_URL master
  64. $ git push origin master

  65. None
  66. Runtime

  67. Backend code Frontend code Kotlinx.serialization JAR JS bundle ktor React

    Data transfer objects https://commons.wikimedia.org/wiki/File:Google_Chrome_icon_(September_2014).svg https://github.com/dokku/dokku
  68. Backend code Frontend code Kotlinx.serialization JAR JS bundle ktor React

    Data transfer objects https://commons.wikimedia.org/wiki/File:Google_Chrome_icon_(September_2014).svg https://github.com/dokku/dokku http://www.quickmeme.com/p/3w2euz
  69. https://memegenerator.net/instance/53270254/yeah-thatd-be-great-yeah-if-you-could-just-show-me-some-code-thatd-be-great

  70. https://github.com/wiyarmir/keynotedex

  71. https://keynotedex.wiyarmir.es/

  72. What next?

  73. •kotlinx-io: Multiplatform I/O •Android and iOS clients •React Hooks

  74. Slides: https://speakerdeck.com/wiyarmir/full-stack-kotlin Code: github.com/wiyarmir/keynotedex Contact: twitter.com/@wiyarmir