Share
Embed
Unravelling materialization process in Jetpack Compose
This session goes over the process used by ComposeUi, in constructing the actual ui.
Gibson Ruitiari
November 26, 2022
Transcript
79 slides
-
👋 About me 📛 Gibson Ruitiari 🎒 Law Student 💻
Hobbyist Android + Kotlin developer 💁♂️ Manga+comics 🐙 https://github.com/GibsonRuitiari -
setContent { // houses ui- building blocks such as Box,
Lazy Layout, Text etc Text(text=“hello world”) } -
@Composable fun HelloWorld():Unit Execution of this composable Leads to scheduling
of changes Changes involve inserting, moving, replacing ui Nodes within the layout tree -
Root Composable (setContent) HelloWorld Composable Text(“Hello World”) Layout Node Tree
• Replaced • Removed • Inserted -
Compose Ui-Building blocks, such as Box, Lazy Column, Text, Image
etc are modelled as Layouts -
All Layouts emit the same type of UI-Node : Layout
Node Compose Ui-Building blocks, such as Box, Lazy Column, Text, Image etc are modelled as Layouts -
Layout Node represents a UI-Block All Layouts emit the same
type of UI-Node : Layout Node Compose Ui-Building blocks, such as Box, Lazy Column, Text, Image etc are modelled as Layouts -
Layout Node is also responsible for inserting, removing, reordering its
children Layout Node contains a list of all its children -
Multiple connected Layout Nodes make up what we call the
Ui Tree Layout Node is also responsible for inserting, removing, reordering its children Layout Node contains a list of all its children -
Layout Node can only have one parent, which is also
a Layout Node Multiple connected Layout Nodes make up what we call the Ui Tree Layout Node is also responsible for inserting, removing, reordering its children Layout Node contains a list of all its children -
*Each parent can only have one owner Layout Node can
only have one parent, which is also a Layout Node Multiple connected Layout Nodes make up what we call the Ui Tree Layout Node is also responsible for inserting, removing, reordering its children Layout Node contains a list of all its children -
@Composable fun Box(modifier: Modifier) { Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier
=modifier) } 😊 Example with the good Ol’ Box -
How does a Composable emit Layout Node? Re-usable Compose Node
Box Layout Node Composition -
@Composable inline fun Layout( content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val viewConfiguration = LocalViewConfiguration.current -
// emits the ReusableComposeNode ReusableComposeNode<ComposeUiNode, Applier<Any>>( factory = { LayoutNode()
}, update = { set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) set(viewConfiguration, ComposeUiNode.SetViewConfiguration) }, skippableUpdate = materializerOf(modifier), content = content ) } -
• When the Key changes, the content of the Reusable
compose node is updated • These UI-Nodes have keys that identify them Why ReusableComposeNode -
• Changes happening to this node leads to the particular
node being updated • When the Key changes, the content of the Reusable compose node is updated • These UI-Nodes have keys that identify them Why ReusableComposeNode -
• The Compiler does not discard/create a new node, instead
it updates it • Changes happening to this node leads to the particular node being updated • When the Key changes, the content of the Reusable compose node is updated • These UI-Nodes have keys that identify them Why ReusableComposeNode -
For Ui-Node to be re-usable it must have update and
set methods in the emit call update = { set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) set(viewConfiguration, ComposeUiNode.SetViewConfiguration) }, This is why all Layout Nodes are modelled as ReusableComposeNode -
update = { set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) set(viewConfiguration,
ComposeUiNode.SetViewConfiguration) }, Properties are set during the initial composition Updated during re-compositions if The properties have changed -
Turning the Layout Nodes making up the Node Tree to
the actual UI ⭐ -
• It is the work of the client ui library,
as in this case, ComposeUi [Android] to change the UI-Nodes in the Tree to the actual UI, that can be seen by the User -
• This process is what is referred to as Materialization
• It is the work of the client ui library, as in this case, ComposeUi [Android] to change the UI-Nodes in the Tree to the actual UI, that can be seen by the User -
Applier UI Applier <Layout Node> Runtime Android Implementation Of the
Applier Compose Ui Abstract Applier -
• Applier is an abstraction used by the runtime to
materialize the Ui Nodes • Base Implementation of the Applier is the Abstract Applier, that is used by Client Libraries to make their own Applier implantation -
• During materialization, the Applier traverses through the whole tree
• Visited nodes are stored in a stack • The reference of the current visited Ui-Node is held by the Abstract Applier to know which node it should perform operations on -
The Applier performs operations such as updating the Node’s content,
on the inserted node When a node is visited, the node is pushed/inserted into the stack -
The Applier performs operations such as updating the Node’s content,
on the inserted node Once the operations are done, the node is popped out of the stack, and the Applier moves to the next node in the tree When a node is visited, the node is pushed/inserted into the stack -
@Composable fun ContentComposable(parentViewModel:ParentViewModel, modifier:Modifier=Modifier){ val isLoading by parentViewModel.isLoading.collectAsState() Column(modifier=modifier.fillMaxSize()) when{
is Loading->{ // 1st text Text(“I am loading…”) } else->{ // 2nd text Text(“Yeii Content to be shown”) } }} -
Column Loading state changes Applier transverses the tree by visiting
the Column Layout Node Inserts /Deletes the 1st or 2nd Text() depending on the Loading State Current visited Node Text Text -
The popping and insertion of nodes into the Stack are
carried out by the Abstract Applier -
Existing implementations therefore do not need to implement their own
navigation logic The popping and insertion of nodes into the Stack are carried out by the Abstract Applier -
Existing implementations therefore do not need to implement their own
navigation logic Implementation of the AbstractApplier in ComposeUi [Android client] is the UiApplier The popping and insertion of nodes into the Stack are carried out by the Abstract Applier -
The UiApplier is responsible for materializing LayoutNodes in the tree
Existing implementations therefore do not need to implement their own navigation logic Implementation of the AbstractApplier in ComposeUi [Android client] is the UiApplier The popping and insertion of nodes into the Stack are carried out by the Abstract Applier -
Strategy used by Appliers to build the tree Top-down approach
Bottom-up approach -
Bottom up Approach Nodes are inserted bottom –up, to avoid
duplicate notification of ancestral nodes Row -
Row Text Text Inserting the two texts composables Notifies the
Row composable only, -
Nesting of composables in Jetpack compose is not a problem
because of the bottom-up approach used By the Ui Applier -
Therefore, unlike when you are using xml, in Jetpack compose
you do not need to flatten your UI hierarchies Nesting of composables in Jetpack compose is not a problem because of the bottom-up approach used By the Ui Applier -
internal class UiApplier( root: LayoutNode ):AbstractApplier(root) { override fun insertTopDown(index:
Int, instance: LayoutNode) { // Ignored. Android uses bottom up strategy } override fun insertBottomUp(index: Int, instance: LayoutNode){ current.insertAt(index, instance) } override fun remove(index: Int, count: Int) { current.removeAt(index, count) } -
Top Down Approach Building of the Ui tree starts from
top to down, This approach is used by Vector Appliers Row Text Text -
Multiple notifications of ancestral nodes can cause performance issues, hence
why This strategy is only used by Vector Applier In top-down approach, during node insertion, all ancestors are notified -
We know how to build the Ui Tree but how
do we exactly do we draw the actual Ui? -
Owner (AndroidComposeView) Jetpack compose is just a UI framework, independent
of its implementations. -
Existing implementations of Jetpack compose, thus must have an integration
point Owner (AndroidComposeView) Jetpack compose is just a UI framework, independent of its implementations. -
All the Android related properties such as context, are provided
to ComposeUi by the AndroidComposeView The Android compose view connects/bridges the Layout Nodes with Android Views -
AndroidComposeView is actually a view group All the Android related
properties such as context, are provided to ComposeUi by the AndroidComposeView The Android compose view connects/bridges the Layout Nodes with Android Views -
Whenever a node is added, updated or removed from the
tree, the Android compose view Invalidates itself -
After invalidation, changes are reflected onto the screen during the
next draw pass Whenever a node is added, updated or removed from the tree, the Android compose view Invalidates itself -
Example: Node insertion internal fun insertAt(index: Int, instance: LayoutNode) {
check(instance._foldedParent == null) { "Cannot insert $instance because it already has a parent." + " This tree: " + debugTreeToString() + " Other tree: " + instance._foldedParent?.debugTreeToString() } check(instance.owner == null) { "Cannot insert $instance because it already has an owner." + " This tree: " + debugTreeToString() + " Other tree: " + instance.debugTreeToString() } instance._foldedParent = this _foldedChildren.add(index, instance);onZSortedChildrenInvalidated() /** Other code… omitted by me for brevity☺ */ -
New Node List<LayoutNodes> List of children maintained by parent node
Get’s added to the list List is sorted according to the Z index of the Children present in the list Node is attached the same owner as the parent Owner is requested by the parent to re-measuring of the new node and the parent View is flagged as “dirty” -
Owner [AndroidComposeView] calls invalidate / requestLayout() Owner [AndroidComposeView] calls dispatchDraw
method Owner [AndroidComposeView] calls onMeasure() -
Root Layout Node draw() method is invoked, Layout Nodes of
the tree are drawn -
*Process for removing/inserting Layout Nodes follows a similar procedure *Layout
Nodes are drawn according to their zIndex -
*Since the parent houses the newly inserted node, it has
to request the owner [AndroidComposeView], to remeasure it together with the its child [the newly inserted node] *Process for removing/inserting Layout Nodes follows a similar procedure *Layout Nodes are drawn according to their zIndex