Composing in the wild

Building a web application with Compose as a server.

Jetpack Compose is a upcoming declarative UI framework for Android with many new features provided by its compiler plugin and smart runtime. As a long-time Kotlin enthusiast, I was amazed when it was announced a year ago and still discover something new in there every now and then.

However, the Compose story doesn’t end on “just” building UI. Even during the first talks describing magic behind the curtain, the library goal was defined as “to efficiently build and maintain tree-like data structures”. It opens a door to create your custom dynamic hierarchies (not necessarily tied to visuals) with all the benefits of caching and incremental updates.

It is still quite hard to imagine tree structures dynamic enough to require Compose, practical enough to implement, and far enough from UI at the same time. After going back and forth, I decided to experiment with managing the structure of a webpage, similar to how SwiftUI was applied before.

What exactly does it do? It allows you to define a webpage and user interactions in a Compose way, similar to its Android implementation. Of course, it is hard to port many concepts directly, as you can not, for example, use constraints for laying out elements. However, if you squint hard enough, it does look quite similar to how you could write it on mobile.

The snippet above results in the following screen:

So what exactly happening here and how does it work?

Composing a Web page

Managing the DOM is extremely close to the original goal Compose was created for. It is a tree-like structure of visual elements (check!) which is also highly dynamic, while updating with user input (check!!). The frontend community has produced many interesting solutions in JavaScript (including this one-pager), so it seems feasible to try Compose here as well. The only problem is: at the time of writing this article, none of its components actually work in the browser.

As execution on the client wasn’t an option, I went for the same approach as SwiftWebUI author did: running it on a server. This is how it works:

  • Compose starts with some initial state which results in the server-side representation of a webpage.
  • This representation is sent to the client as commands of adding/removing/moving nodes through websocket. The client then changes the structure of the webpage according to commands received from the server.
  • When a client event happens (for example click), it is sent to the server. Here, the state is updated with the new changes. It yields a different structure of the DOM, and these updates are sent to the client, refreshing the webpage.

This implementation still puts a lot of constraints on what we can do in terms of interactivity. For the scope of this experiment, however, it is more than enough.

Composing on the server side

Compose is developed as a purely Android library, so many pieces won’t work for the server. Thankfully, developers have separated it into multiple packages and we can pick and choose whatever we want. For example, we have compose-runtime with under-the-hood machinery, ui module for basic interaction with Android systems, ui-material for implementation of material components and themes, and many more. The first one (the runtime) is what we need to adapt to make this project possible.

To launch the runtime in a server environment, we need to substitute everything that links it to Android with server counterparts. UI frameworks are often tied to the main thread and other platform specifics, so it may seem quite hard. In reality, it was super easy, barely an inconvenience: JetBrains is currently collaborating with Google to bring Compose to multiplatform, so most of the required expect / actual bindings were already there.

You can see the full list of required bindings here. To highlight, we mimic the main thread with a single thread CoroutineDispatcher and stub some other APIs based on their definition for desktop.

For the server environment, Ktor was the best choice. It is a web server developed by JetBrains which has perfect integration with coroutines and exposes configuration as expressive DSL. An additional package gives us websockets, and we can use feature extension API to integrate Compose into the server seamlessly.

Composing a tree

After the runtime is up and running, it is time to create a representation of a webpage on a server side. Running through the internals of already existing Compose elements, I ended up with the following structure:

This representation of the DOM distinguishes two types of elements:

  • Tag, which contains the name of the HTML node and may have some children. Order of child nodes is manipulated through public methods with simple list operations.
  • Text, always a leaf node with text values only.

These nodes can now be expressed as @Composable functions:

We need an ApplyAdapter which controls how we add nodes to the tree. Here, we delegate to methods of aTag class for all operations. This adapter is then used with emit function to link @Composable functions to the tree by interacting with the composer. It has three parameters to control the resulting structure:

  • Constructor, describing how the node is created. Every change in the captured parameter here will result in Compose recreating the node. tag function is the perfect example: in the browser, most tags have different representations. For instance, <input> actually has underlying type of HTMLInputElement, so, when changing tag to <div>, we need to create new HTMLDivElement to replace it.
  • Update, defining how the element is updated. It is useful to avoid removing/adding the element every time a simple value update will suffice. text is a good example of this, we don’t need to replace the whole node every update, changing inner value instead.
  • Children, propagating other elements down the tree.

Now we can use these definitions to build a simple structure from tags:

Composing through a WebSocket

After building the tree, the next tricky part is client-server communication. It consists of two parts: passing node updates to the client and sending events back.

Node updates are mostly caused by client events in our case. For example, clicking on a button can display/hide an element:

Compose already does the diffing for tree changes (add/move/remove), so we can hook into ApplyAdapter methods and dispatch commands to the client from there. It also takes care of the initial state, which is sent to the client as a sequence of additions. Properties (like value in Text) are updated without calls to ApplyAdapter, but we can catch them through a custom setter.

To make this work, every node needs to have access to a websocket connection. It would be quite annoying to pass it through each level of calls, and Compose has a utility just for that. Ambient allows us to propagate some elements without explicitly keeping them as parameters. It is used in Android to provide Context or Theme, so we will do a similar thing for the server:

The second part (client events) was a bit harder to tackle. The main problem is to make sure the event is propagated to the right node and there it invokes the right callback. After playing around with different ways of expressing them, I have settled on the following structure:

  • Each node now has a new id field, which serves as a method of matching them between client and server.
  • We introduce a new entity that is similar to CommandDispatcher but works the other way around, listening to websocket and distributing updates to events.
  • Each event is defined with the following structure:

It consists of three parts: type serves as an identifier, Payload provides a way to pass values from the client in a type-safe way, and Callback invokes lambda with payload to update things in Compose world. All those elements are matched by descriptor, which ties them together inside the node.

This system is not ideal, but it is extensible (allows you to define custom events) and customizable (those events can be tied to any JS/Server code). You can also check out definitions of other events here or how to process them on the server or client.

Composing it better

With the composition and events running, now it was the time to think about expanding functionality. On this stage, I created a couple of components which defined tags, but the parameters were getting a bit out of control:

Experimenting with inline CSS, I realized that it required a much more flexible system. Looking for inspiration in original libraries, I found out that many similar cases were handled with Modifier instances, and it worked for me perfectly as well.

Modifiers create a linked list where elements are chained with others using Kotlin extension functions. Using them hides optional parameters from the signature in a type-safe way, converting the example above into this:

This system provides some basic modifiers, and later they can be extended with more concrete ones. For example, all HTML tag attributes are defined as a basic modifier of attribute(String, String). Later they are refined with more precise definitions, e.g. with type(String) on input and so on. Taking this further, we can provide scoping (flex parameters inside of display: flex parents) and type safety (enums or integers instead of strings as parameters).

Jetpack Compose is much more than a UI toolkit made exclusively for Android. I can definitely see how it can be applied in multiplatform to power many Kotlin projects.

At the same time, API surface of the core functionality is very minimal. The compiler plugin takes the route of minimal invasion, providing restartable functions with cacheable calls transparently for the user. This is then leveraged by runtime with all the tree building functionality and other tooling. I am very surprised of all the new possibilities provided with such a small change to the language.

The project was a huge learning experience for me, pushing the boundaries of what Compose can do on its current stage of development. The approach I have taken puts a barrier on many interactions, as it is relying on fast communication between client and server, which is not the case for the majority of users. It can certainly be improved with a browser runtime whenever Compose supports either JS or WASM in full.

The source code is available on Github and is separated into 4 modules:

You can try it yourself online, or play with the repo using deploy branch, which keeps prebuilt jar for compose-runtime.

You can find me on Twitter, where I am sharing random thoughts about Android, Kotlin, and other things I find exciting along the way.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store