Cartographing Jetpack Compose: compiler and runtime
Thomas Künneth
Posted on May 9, 2021
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"
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)
prints
I/System.out: function test (Kotlin reflection is not available)
On the other hand function references of @Composable
functions are not currently supported.
Another important thing to recall is that @Composable
invocations can only happen from the context 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 themeasurePolicy
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:
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:
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)
Posted on May 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.