Grokking Android

Getting Down to the Nitty Gritty of Android Development

The Different Node Types in Jetpack Compose

By

Teaser showing a partial listing of ComposeUiNode

If you look at Compose it's weird. You have tons of functions that return nothing. Yet the Android documentation says:

Composable functions emit UI hierarchy.

What does this actually mean? Well, from a functional programming point of view your innocuous looking stateless composable functions create loads of side effects1). Among other things they create nodes.

Sometimes you might create Composables that create nodes yourself. But more often you will simply delegate to other Composables that then create the nodes.2)

So let's have a look at what nodes we have, where they are created and what purpose they fulfill:

ComposeUiNode

When you dig into the Layout composable you see that it calls ReusableComposeNode:3):


@Composable
@UiComposable
inline fun Layout(modifier: Modifier = Modifier, measurePolicy: MeasurePolicy) {
    val compositeKeyHash = currentCompositeKeyHash
    val materialized = currentComposer.materialize(modifier)
    val localMap = currentComposer.currentCompositionLocalMap
    ReusableComposeNode<ComposeUiNode, Applier>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(materialized, SetModifier)
            set(compositeKeyHash, SetCompositeKeyHash)
        },
    )
}

ReusableComposeNode is no node in itself. What you see in the code above is no constructor call. It's instead just a call to yet another Composable. A tiny bit innocuous is the factory argument in line eight - even though it is very important. Because that's the function that eventually will create a node. It will create a ComposeUiNode.

The ReusableComposeNode Composable is highly tied to the Compose runtime. That's the code, that tells the composer that a new group of type GroupKind.ReusableNode should be started and then the code either creates a new node or reuses the existing one if we're recomposing. And finally it set's its content when the update function argument is used:


@Composable
inline fun <T : Any, reified E : Applier> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater.() -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    Updater(currentComposer).update()
    currentComposer.endNode()
}

We see that the factory get's passed on to the createNode() call. This call schedules the node to be created when all insertions are processed. The compose runtime defers many things internally to allow for optimizations. Here we're not interested when this actually happes, suffice is to say, that it will happen. And when it happens, the factory is going to be called.

Now let's look at the factory itself a bit closer: ComposeUiNode.Constructor seems to be the constructor of the ComposeUiNode. But that's once again not what it seems to be. Instead ComposeUiNode is an interface that in itself doesn't do anything. It's actually the base interface used by LayoutNode - and the reason for this interface and this constructor method is given in the comment above the interface declaration:


/** Interface extracted from LayoutNode to not mark the whole LayoutNode class as @PublishedApi. */
@PublishedApi
internal interface ComposeUiNode {
    // ...
    /** Object of pre-allocated lambdas used to make use with ComposeNode allocation-less. */
    companion object {
        val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
        val VirtualConstructor: () -> ComposeUiNode = { LayoutNode(isVirtual = true) }
        // ...
    }
}

So ComposeUiNode is only an abstraction of LayoutNode.

LayoutNode

In the section above we have seen when a LayoutNode actually get's created. Basically whenever the Layout composable is called. The LayoutNode is the element in the tree that represents something on the screen. It needs to be measured, it might place children within its bounds and it might draw content.

Each LayoutNode knows of its children and of its parent. So LayoutNodes form a tree of nodes that represents the stuff that was emitted. To be clear: Compose creates a tree just like any other UI framework - but Google preferred to use some fancing naming for that. So whenever you hear/read about Composables emitting something, think about a LayoutNode getting created and inserted into the tree.

The LayoutNode class is actually pretty interesting and thus merits a blog post on it's own, where I will cover some aspects of it in more detail.

Two things are worth mentioning here:

I will cover both, the Applier as well as the Owner in seperate blog posts since both are very important concepts when it comes to Compose.

The purpose of a LayoutNode

LayoutNodes form the UI tree and keep track of their parent node and child nodes. The management of the UI tree itself is done by the Applier, which I will cover in another post.

And - also very important to keep in mind - the LayoutNode get's stored in the SlotTable.4) Thus when recomposing the runtime can reuse existing LayoutNodes in case it decides that no change is required for this part of the tree.

The LayoutNode also holds onto it's modifiers (see next section) and delegates to them to decide how much place is needed by this LayoutNode (measuring), where to place stuff on the screen (layouting) and finally what to show on the screen (drawing).

There's more to it - but as I mentioned that's part of another post.

Modifier.Node

Modifiers are internally represented by Modifier.Node objects. According to the doc it is the "longer-lived object that is created for each Modifier.Element applied to a androidx.compose.ui.layout.Layout."

This "longer-lived" is interesting. Basically your Modifier.Node lives for as long as it belongs to the modifier chain of a LayoutNode. The LayoutNode holds onto an object of type NodeChain which keeps a list of modifiers internally and checks that, if this list is changed, the lifecycle methods of Modifier.Node - like onAttach() etc. - are called.

There are many existing subtypes of Modifier.Node - for example LayoutModifierNode (see below) or DrawModifierNode. The latter is responsible for actually drawing content on the screen, which I also plan to cover in a separate post.

Since LayoutNodes are part of the SlotTable the Modifier.Nodes that are attached to a LayoutNode's NodeChain object obviously are part of that.

A special subtype: LayoutModifierNode

LayoutModifierNodes are the nodes that actually measure since they are the nodes that affect the size and placement on screen. As the documentation states: "A Modifier.Node that changes how its wrapped content is measured and laid out.". So whenever the chain of Modifier.Nodes is traversed during measuring and layouting this nodes actually start the measuring.

Thus all modifiers that want to change the placement of it's children or have any effect on the size of the overall Composable need to implement LayoutModifierNode. On example for doing that is the SizeNode which is the actual node used by modifiers like height(), width() or size().

SemanticsNode

When you want to pass some information to the accessibility services of the device, you use the semantics modifiers in Compose to tell the system which semantics properties to convey to the user.

But the accessibility services5) do not know of Compose. On other platforms that's obvious but given that Compose is backwards compatible (and on top of that not part of the deliverables of the Android framework), that's true on Android as well.

For accessibility the aforementioned Owner comes again into place. It holds onto a SemanticsOwner which acts as a bridge to the semantics framework of the respective platform.

To do so, the SemanticsOwner holds onto a tree of SemanticNode objects to describe the contents of the screen to users of accessibility services. Another thing I intend to cover in more detail in the future.

So stay tuned for more insights into Compose inner workings. Happy coding!

Footnotes

Footnotes
1 I use the term "side effects" in this paragraph as it has been used for ages in computer science and functional programming. I do not speak here about the side effects mentioned in the official Compose documentation. The Compose documentation at least in the past sometimes alluded to this use of side effects - though in actual fact Compose functions are anything but pure functions.
2 If you look at a Composable after the Compose Compiler plugin has done it's work, you will notice that it not only creates nodes, but that your function makes plenty of calls that change the global state of your app. But what they don't do, is drawing anything. At best they create draw operations that are recorded to be then called at some point in the future, when the system decides that the time for drawing has come. I will cover both topics, the drawing and the changes the compiler makes to your code, in two seperate posts in the future.
3 Actually, there are three Layout composables. But all three call down the road ReusableComposeNode. The sample shown would be very similar for the other variants.
4 I most likely won't cover the SlotTable in this series. I recommend this post about compose by Richard Leland and also for how to use the slottable Mohit Sarveiya's videos about the SlotTable.
5 The accessibility services on a device are AccessibilityService implementations on Android but different things on iOS the desktop or the web, thus I use the general term "accessibility services" in this post.

Wolfram Rittmeyer lives in Germany and has been developing with Java for many years before diving headfirst into Android development (with Kotlin in later years).

He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.

You can find him on Mastodon and LinkedIn.