Fork me on GitHub

Why another web framework, aren’t there already too many to choose from?

Let’s say you’re fairly comfortable with the Kotlin/Java ecosystem, you need to build a rich web application that looks like it belongs in 2018, but you really don’t feel like grappling with the massive and complex JavaScript ecosystem.

I’ve seen this too many times, and so I decided to fix it, and Kweb is the result of that ongoing endeavor.

So what is Kweb?

Kweb is a library for building web applications in the Kotlin programming language that takes quite a unique approach.

Kweb allows you to interact to the browser DOM directly as if it was local to the web server. This process is efficient, minimizing browser-server chatter and browser rendering overhead. DOM fragments are also cached in the browser for an extremely responsive UI, and events are conveyed seamlessly between client and server to maintain consistent state across both.

For example, here we create a <p> element and set its text:

import io.kweb.*
import io.kweb.dom.element.*

fun main(args : Array<String>) {
    Kweb(port = 8091) {
        doc.body.new {
            p().text("Hello world with Kweb!")
        }
    }
}

Kweb has plugins for JavaScript libraries like JQuery and others. It’s also surprisingly easy to build your own plugins for other JavaScript libraries, or extend those Kweb already supports.

Kweb’s Features

  • Build websites in Kotlin
  • Makes the barrier between web-browser and web-server largely invisible to the programmer
  • Combines surprising convenience for the programmer with efficiency under the hood, minimizing client-server chatter and browser render times
  • Seamlessly integrates with powerful JavaScript libraries like Semantic UI (for which we have a Kweb plugin offering a fairly comprehensive Semantic UI DSL)
  • Bind DOM elements in the browser directly to persistent state on the server and have them update automatically, through the observer and data mapper patterns, using the Shoebox persistent state store.
  • Easy to add to an existing project, Kweb is just a library, it doesn’t seek to tell you how your project should be organized
  • Update your web browser instantly in response to code changes

How does it work?

Kweb keeps all of the logic server-side, and uses efficient websockets to communicate to web browsers. We also take advantage of Kotlin’s powerful new coroutines mechanism to efficiently handle asynchronicity, largly invisibly to the programmer.

Can I see an example?

Here is the (heavily commented) main code for a simple to-do list app. You can find the full app here:

fun main(args : Array<String>) { 
    /** A simple yet flexible plugin mechanism */
    val plugins = listOf(semanticUIPlugin)

    /** Create a Kweb instance, and configure it to use the Semantic
     * UI framework. Build a simple to-do list app listening on
     * http://localhost:8091/
     * */
    Kweb(port = 8091, debug = true, plugins = plugins) {
        doc.body.new {

            /** Kweb allows you to modularize your code however suits your needs
                best.  Here I use an extension function defined elsewhere to
                draw some common outer page DOM elements */
            pageBorderAndTitle("Todo List") {

                /** A KVar is similar to an AtomicReference in the standard Java
                    Library, but which supports the observer pattern and `map`
                    semantics.  Here I set it to the current URL of the page.

                    This will update automatically if the page's URL changes, and 
                    if it is modified, the page's URL will change and the DOM will
                    re-render _without_ a page reload.  Yes, seriously. */
                val url: KVar<URL> = doc.receiver.url(simpleUrlParser)

                /** s.content uses the semanticUIPlugin to use the excellent
                    Semantic UI framework, included as a plugin above, and implemented
                    as a convenient DSL within Kweb */
                div(s.content).new {

                    /** Note how url.path[0] is itself a KVar.  Changes to firstPathElement
                        will automatically propagate _bi-directionally_ with `url`.  This
                        comes in very handy later. */
                    val firstPathElement: KVar<String> = url.path[0]

                    /** Renders `firstPathElement`, but - and here's the fun part - will
                        automatically re-render if firstPathElement changes.  This is
                        a simple, elegant, and yet powerful routing mechanism. */
                    render(firstPathElement) { entityType ->
                        when (entityType) {
                            ROOT_PATH -> {
                                val newListId = createNewList()
                                url.path.value = listOf("lists", newListId)
                            }
                            "lists" -> {
                                /** Renders can be nested, which means that only this
                                    specific part of the page must be re-rendered if
                                    url.path[1] changes, which is very convenient
                                    for the developer in comparison to other frameworks,
                                    while minimizing server-browser chatter. */
                                render(url.path[1]) { listId ->
                                    try {
                                        /** Here I use the same render mechanism to tie DOM
                                            state to persistent state stored in Shoebox,
                                            Kweb's simple but powerful key-value store with
                                            observer pattern support.  */
                                        renderList(toVar(State.lists, listId))
                                    } catch (e: NoSuchElementException) {
                                        throw NotFoundException("Can't find list with id $listId")
                                    }
                                }
                            }
                            else -> {
                                throw NotFoundException("Unrecognized entity type '$entityType', path: ${url.path.value}")
                            }
                        }
                    }
                }
            }
        }
    }
}

Next: Setting Up »»