Cartographing Jetpack Compose: compiler and runtime

tkuenneth

Thomas Künneth

Posted on May 9, 2021

Cartographing Jetpack Compose: compiler and runtime

Jetpack Compose is expected to become the preferred ui toolkit for Android and perhaps Kotlin-based Desktop apps. In several articles I have demonstrated how to use it. This one and its follow-ups will contain a more exploratory approach.

Curious? Read on.

As of May 2021 Jetpack Compose consists of six broad areas:

  • compose.compiler transforms @Composable functions and enables optimizations with a Kotlin compiler plugin
  • compose.runtime provides the fundamental building blocks of Compose's programming model and state management, as well as the core runtime for the Compose Compiler Plugin to target
  • compose.foundation provides basic ingredients for Compose applications
  • compose.ui contains the fundamental components of a composable UI needed to interact with the device, including layout, drawing, and input
  • compose.animation helps building, well, animations
  • compose.material provides ready to use Material Design components

Have you noticed that I listed those areas in a specific order? While you can easily write wonderful Compose apps that do not use animation or material you will almost certainly need runtime and foundation. How about the first one, compiler?

androidx.compose.compiler

Well, while it is of utmost importance to get the machinery going, you do not need to include it in your build.gradle file. Let's do it anyway, so that we can peek inside a little:

implementation "androidx.compose.compiler:compiler:$compose_version"
Enter fullscreen mode Exit fullscreen mode

A look into the Compose Compiler

Doesn't look like we would want to invoke any of these manually, right? 🤣

Please recall: while (besides being annotated with @Composable) composables look like ordinary Kotlin functions, they are heavily processed during compile time.

fun test() = ""

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val a = ::test
    println(a)
Enter fullscreen mode Exit fullscreen mode

prints

I/System.out: function test (Kotlin reflection is not available)
Enter fullscreen mode Exit fullscreen mode

On the other hand function references of @Composable functions are not currently supported.

image

Another important thing to recall is that @Composable invocations can only happen from the context of a @Composable function.

An error message regarding the invocation of a composable function

Talking about @Composable inevitably brings us to the second area, as the annotation is located in package androidx.compose.runtime.

androidx.compose.runtime

As of May 2021 its description is rather sparse. We have 14 interfaces with interesting names like Applier, Composer, Composition and State. And 12 classes, among them Recomposer and Updater. One enum (Recomposer.State) and 14 annotations.

For example, @Stable

is used to communicate some guarantees to the compose compiler
about how a certain type or function will behave.

And NonRestartableComposable

can be applied to Composable functions in order to prevent code
from being generated which allow this function's execution to be
skipped or restarted.

Now, which of these are parts of the Compose clockwork, and which will we be using in our apps?

Assume, at some point you are invoking androidx.compose.material.Text("Hello Compose"). The resulting call chain has quite a few steps, among them are BasicText() and CoreText(). Both reside in androidx.compose.foundation.text. CoreText(), by the way, has an internal modifier. More importantly, it invokes yet another composable, Layout() in package androidx.compose.ui.layout.

The docs say:

Layout is the main core component for layout. It can be used
to measure and position zero or more layout children.

The measurement, layout and intrinsic measurement behaviours of
this layout will be defined by the measurePolicy instance. See
MeasurePolicy for more details.

For a composable able to define its content according to the
incoming constraints, see
androidx.compose.foundation.layout.BoxWithConstraints.

Let's peek inside Layout.kt:

The Layout() composable

With the invocation of ComposeNode we make our way back to package androidx.compose.runtime. The docs say:

Emits a node into the composition of type T. Nodes emitted
inside of content will become children of the emitted node.

Again, some code:

The ComposeNode() composable

Without going into too much detail we see heavy use of currentComposer, which is an instance of the Composer interface. It is based upon the concept of nodes. Updater is

A helper receiver scope class used by ComposeNode to help
write code to initialized and update a node.

Take aways

At this point you might recall the title of this article, Cartographing Jetpack Compose, and ask yourself if we still are on track. We are, as we now have quite some understanding of the innards of Compose. We have seen that invoking composables leeds to building and updating node-based structures. How they become - or lead to - what we see on screen is another interesting story I may be telling later.

In this episode we have cartographed two areas, compose.compiler and compose.runtime. The first one is very important, yet uninteresting from an apps' perspective. The latter one both drives the (re-)composition of the ui and offers key elements like @Composable and Layout. We have already seen foundation briefly. Please stay tuned for a more detailed look.

Did you like this article? Please share your thoughts in the comments.


Screen orientation in Jetpack Compose (4 Part Series)
Drawing and painting in Jetpack Compose (3 Part Series)
On (de)composing action bars (3 Part Series)

💖 💪 🙅 🚩
tkuenneth
Thomas Künneth

Posted on May 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related