Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Creating Full-Stack Web Apps with Kotlin DSLs

pahill
December 06, 2019

Creating Full-Stack Web Apps with Kotlin DSLs

Domain-specific languages (DSLs) are a great way to create beautifully readable code. This session will explore why Kotlin's syntax lends itself to creating elegant DSLs by examining how to build a full-stack web app with:
- Ktor to create an asynchronous backend
- Exposed to develop a data access layer
- kotlinx.html to build a frontend

I will introduce each framework/library briefly, illustrate the DSL using examples, and explain why it’s an example of a great DSL. By the end of the session, you will have a new appreciation of the beautiful DSLs that can be created using Kotlin!

pahill

December 06, 2019
Tweet

More Decks by pahill

Other Decks in Programming

Transcript

  1. HELLO MOLO HALLO THOBELA DUMELA LUMELA ABUSHENI SANIBONA AVUWANI SALIBONANI

    SAWUBONA HELLO MOLO HALLO THOBELA DUMELA LUMELA ABUSHENI SANIBONA AVUWANI SALIBONANI
  2. Wikipedia A VISION BOARD IS A COLLAGE OF IMAGES, PICTURES,

    AND AFFIRMATIONS OF ONE'S DREAMS AND DESIRES, DESIGNED TO SERVE AS A SOURCE OF INSPIRATION AND MOTIVATION.
  3. Ward Cunningham YOU CAN CALL IT BEAUTIFUL CODE WHEN THE

    CODE MAKES IT LOOK LIKE THE LANGUAGE WAS MADE FOR THE PROBLEM.
  4. What makes Kotlin a great language for DSLs? Optional semicolon

    Infix functions No parentheses for last lambda argument Operators overloading using conventions Extension functions Lambdas with receivers Invoke convention Venkat Subramaniam, Creating Internal DSLS in Kotlin, Kotlinconf 2018
  5. Lambdas with receivers Lambdas that are executed within the context

    of a receiver. val isEven: Int.() -> Boolean = { this % 2 == 0 } println(294823098.isEven())
  6. Ktor is a Kotlin framework for creating asynchronous servers and

    clients Built on Kotlin and coroutines Easy setup using IntelliJ Plugin or generator at start.ktor.io Use DSL to specify application behaviour ThoughtWorks Trial category since April 2019
  7. ThoughtWorks, April 2019 THE FLEXIBILITY TO INCORPORATE DIFFERENT TOOLS IN

    ADDITION TO ITS LIGHT-WEIGHT ARCHITECTURE- MAKES KTOR AN INTERESTING OPTION FOR CREATING RESTFUL SERVICES
  8. Client Server Convert to ApplicationCall Setup Monitoring Features Call HTTP

    1.x HTTP 2.x Websockets Fallback Pipeline of Interceptors
  9. Creating the application fun Application.lifeVisualiser() { //Module feature installations routing

    { //Module routing } } fun main() { embeddedServer(Netty, port = 8080, module = Application::lifeVisualiser ).start(wait = true) }
  10. Setting up the routes (1) routing { get("/inspirations") { ...

    } get("/users/{userId}/inspirations") { ... } } install(ContentNegotiation) { gson { } }
  11. Setting up the routes (2) get("/inspirations") { val keywords =

    call.parameters["keywords"] if (keywords == null) { call.respond(HttpStatusCode.BadRequest) return@get } } val inspirations = api.searchPhotosByKeywords(keywords) if (inspirations != null) { call.respond(HttpStatusCode.OK, inspirations) } else { call.respond(HttpStatusCode.NoContent) }
  12. Route refinement - Locations get<Inspirations.KeywordInspirations> { val inspirations = inspirationApi.searchPhotosByKeywords(it.keywords)

    if (inspirations != null) { call.respond(HttpStatusCode.OK, inspirations) } else { call.respond(HttpStatusCode.NoContent) } } @Location("/inspirations") data class KeywordInspirations(val keywords: String) install(Locations)
  13. Route refinement - Splitting into files MyRoute.kt fun Route.searchInspirationsByKeywords(inspirationApi: InspirationApi)

    = get<Inspirations.KeywordInspirations> { … } routing { searchInspirationsByKeywords(api) ... } Application.kt
  14. Unsplash API Register as a developer Register the app to

    get access key and secret key The API section we’re using doesn’t require OAuth, just an Auth header with the access key Extensive documentation for API use
  15. Setting up the Ktor client private val httpClient = HttpClient(CIO)

    { install(JsonFeature) { serializer = GsonSerializer { serializeNulls() disableHtmlEscaping() } } }
  16. Using the Ktor client override suspend fun searchPhotosByKeywords(keywords: String) :

    List<Inspiration>? { val url = "$unsplashUrl/search/photos?query=$keywords" return httpClient.get<SearchResponse?>(url) { header("Authorization", "Client-ID $accessKey") }?.results?.mapNotNull { mapUnsplashToInspiration(it) } }
  17. Is it beautiful? • First iteration, not so much •

    Following iterations: framework/ language provides means of refactoring away boilerplate and complexity • Minimal setup, even in code • Routing is easy to read and understand
  18. Exposed is a light-weight SQL library written for the Kotlin

    language Created and maintained by JetBrains DSL and DAO flavours Supports many databases
  19. Creating a table object Inspirations : Table() { val id

    = varchar("id", 128).primaryKey() val userId = varchar("userId", 128) references Users.id val photoUrl = varchar("photoUrl", 1024) val title = varchar("title", 100) val description = varchar("description", 1000) val addedDate = long("addedDate") }
  20. A clever little piece of Kotlin code private suspend fun

    <T> dbTransaction(block: () -> T): T { return withContext(Dispatchers.IO) { transaction(database) { block() } } }
  21. Inserting data dbTransaction { Inspirations.insert { it[id] = UUID.randomUUID().toString() it[Inspirations.userId]

    = userId it[photoUrl] = photo.photoURL it[title] = photo.title it[description] = photo.description it[addedDate] = photo.addedDate } }
  22. Querying data Inspirations.select { Inspirations.id eq id }.mapNotNull { }.firstOrNull()

    Inspiration( id = it[Inspirations.id], userId = it[Inspirations.userId], photoURL = it[Inspirations.photoUrl], title = it[Inspirations.title], description = it[Inspirations.description], tags = syncFindTagsByInspirationId(it[Inspirations.id]) ?: emptyList(), addedDate = it[Inspirations.addedDate] )
  23. Is it beautiful? • Follows SQL closely • Use of

    this and it context objects • Select query syntax is concise and expressive
  24. Project setup with npm Install the project creator
 npm install

    -g create-react-kotlin-app Create the project
 npx create-react-kotlin-app lifevisualiser Run in the browser (live reloading) 
 npm start npm start
  25. Creating a component step-by-step 1. Define a properties interface (Optional)

    2. Define a state interface (Optional) 3. Create a component templated with the interfaces 4. Implement the render method (with kotlinx.html DSL) 5. Create a builder
  26. Defining the state interface class PhotoSelectionState : RState { var

    inspirations = mutableListOf<Inspiration>() }
  27. Creating the component class PhotoSelectionComponent() : RComponent<PhotoSelectionProps, PhotoSelectionState>() { override

    fun RBuilder.render() { //Create the HTML } //Other lifecycle functions //Other functions }
  28. Implementing the render method state.inspirations.forEach { inspiration -> div("mdc-layout-grid__cell--span-4") {

    div("mdc-card") { img(src = inspiration.photoURL, classes = "photo-selection-photo") { attrs.height = "200px" attrs.onClickFunction = { props.callback.invoke(inspiration) } } p { +inspiration.title } override fun RBuilder.render() { div("mdc-typography--headline1 inspiration-header") { +"LifeVisualiser" } div("mdc-card main-card") { //Form with search component div("mdc-layout-grid") { div("mdc-layout-grid__inner") {
  29. Implementing the builder fun RBuilder.photoSelector(callback: (app.Inspiration) -> Unit) : ReactElement

    { return child(PhotoSelectionComponent::class) { //Initialise props here attrs.callback = callback } }
  30. Using a JavaScript library directly 1. Install the library 


    npm install axios —save 2. Create external functions and interfaces 3. Use like regular Kotlin
  31. Creating the external functions and interfaces @JsModule("axios") external fun <T>

    axios(config: AxiosConfigSettings): Promise<AxiosResponse<T>> external interface AxiosConfigSettings { var url: String var method: String ... } external interface AxiosResponse<T> { val data: T val status: Number ... }
  32. Using the functions fun getPhotosByKeywords(keywords: String) { val callConfig: AxiosConfigSettings

    = jsObject { url = "${App.BASE_URL}/inspirations/?keywords=$keywords" method = "GET" timeout = 3000 } axios<Array<Inspiration>>(callConfig).then { if (it.status == 200) { val newState = PhotoSelectionState().apply { inspirations.addAll(it.data) } setState(newState) } } }
  33. THANK YOU NGIYABONGA KE A LEBOGA NDZA NKHENSA KE ITUMETSE

    NDO LIVHUWA ENKOSI THANK YOU NGIYABONGA KE A LEBOGA NDZA NKHENSA KE ITUMETSE NDO LIVHUWA ENKOSI BAIE DANKIE THANK YOU NGIYABONGA KE A LEBOGA NDZA NKHENSA KE ITUMETSE NDO LIVHUWA ENKOSI THANK YOU NGIYABONGA KE A LEBOGA NDZA NKHENSA KE ITUMETSE NDO LIVHUWA ENKOSI