When thinking about Compose it's good to notice that Composables don't yield actual UI, but "emit" changes to the in-memory structure managed by the runtime (slot table) via the Composer.
That representation has to be interpreted later on to "materialize" UI from it 🧵
An example of this is the Layout Composable, used to implement UI components. Layout uses ComposeNode to emit changes about how to create, update, index, and store the node on the table. That's why Composables return Unit 😲
Emitting these changes takes place via the compile time injected Composer, and it happens when the composable function is executed. That is during composition ⚙️
Layout belongs to compose-ui 🎨 That is code the developer writes. ComposeNode is part of the runtime, like the Composer and the slot table. See how node type is T in the runtime, so Compose can work with generic nodes to support other use cases.
Layout fixes generic type T to be ComposeUiNode, which is ultimately LayoutNode. In other words compose-ui decides what type of node to use Compose for 💡
This separation between the in-memory representation of the graph and how it is interpreted allows to integrate with other libraries that use different types of nodes, making it possible to write libraries for desktop, web, or non UI related nodes.
The expectation is Composable functions are fast and restartable, so they simply schedule changes instead of building real UI. Composition traverses the 🌲 executing all Composable funcs to make them emit, ultimately filling up the table. It optimizes & prioritizes the process.
Recomposition happens multiple times and for different reasons, one of them being that the data being read by some elements on the tree has varied. That will make those functions to execute again (restart), and therefore emit again and update the table.
Once composition is done its time for materializing the changes from the table. The runtime delegates this to the "Applier", which is an interface implemented by compose-ui. It traverses the structure interpreting and materializing all nodes 🌲
“Materialize” is the verb used in Compose internals to refer to the action of interpreting the tree of changes to finally yield whatever output we are using Compose for. That is UI in the case of Android. The runtime is agnostic of the Applier's implementation.
The UiApplier implementation for Android delegates all actions to insert, move, remove or replace nodes to the node itself, a LayoutNode. LayoutNode is part of the UI library and provides the implementation details on how to materialize itself ✏️
This is where setContent becomes relevant. It adds an AndroidComposeView to the top level window decor view which draws all the Compose LayoutNodes to its own canvas. It sets itself as the owner of all the nodes, which is how it connects them with the View system 🔗
Here is where the relevant Android stuff like config, Context, LifecycleOwner, savedStateRegistryOwner, saveableStateRegistry & the actual owner view is linked and provided down the Composable tree via a CompositionLocalProvider, so you can access those things while coding 👩💻
The ultimate result of applying all the changes is that LayoutNodes are drawn to the AndroidComposeView Canvas whenever it receives the dispatchDraw() order, given it is an Android ViewGroup. ✅
Small disclaimer: If you read this 🧵 keep in mind all links are pointing to current indexed version in cs.android.com and that will likely vary over time since it’s all implementation details. That said, the overall spirit and concepts will remain.
I recommend reading this other thread as a continuation of this one. It clarifies the difference between the change list and the slot table. I was not able to dive that deep yet. Thanks @chuckjaz 👏
In Jetpack Compose, Composable functions build up the Composition tree when they execute for the first time, and then update it on every recomposition.
Here is a thread about Composition and node types in Compose. It covers both Compose UI and the Compose runtime 🧵
Here is a diagram of how recomposition (meaning re-executing Composable functions) updates the node tree representation (Composition).
The way this really happens is by emitting a list of changes for the node tree whenever a Composable function executes. Those changes are packed together as deferred lambdas, and applied in a decoupled phase when the composition or recomposition process is complete 🤯👇
Mosaic by @JakeWharton is a nice case study for how to create a client library for the Jetpack Compose compiler and runtime. Some pointers on where to find the key pieces in Mosaic, if you are interested 🧵
Any client library needs to define its own nodes and teach the runtime how to materialize them (provide an Applier). I.e: Teach it how to build and update the tree.
Here are the Mosaic nodes and the Applier implementation 👇 (2/*)
Nodes know how to attach, measure, layout, & render themselves, so the Applier can simply delegate the actions of adding/removing/moving nodes to them.
This lets the runtime trigger those actions when required without knowing about implementation details of the platform 👍 (3/*)
Some people seems a bit confused about when to use `composed` to write a custom Modifier in Jetpack Compose, and when to use the `then` extension function instead. Well, they are actually very different. Let me expand on this just a bit 🧵
A "standard" modifier is written using the `Modifier.then` function over the modifier object, or a previous modifier instance. All the modifiers we use daily are written like this, making use of an extension function for ergonomics.
To write a modifier, we must pick the correct type. E.g: LayoutModifier if we want to affect measuring and layout. ParentDataModifier, if we need to provide data to a parent during measuring and positioning. DrawModifier, if we need to draw into the Layout. There are many others.