Slide 1

Slide 1 text

Materialization 🌟 in Jetpack compose

Slide 2

Slide 2 text

👋 About me 📛 Gibson Ruitiari 🎒 Law Student 💻 Hobbyist Android + Kotlin developer 💁‍♂️ Manga+comics 🐙 https://github.com/GibsonRuitiari

Slide 3

Slide 3 text

📖 Contents.. How Ui in jetpack compose is drawn

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

But it is all functions how is the Ui drawn?

Slide 6

Slide 6 text

@Composable fun HelloWorld(){ Text(text="hello world!") }

Slide 7

Slide 7 text

@Composable fun HelloWorld():Unit 🔩 runs during composition/ recomposition Emits changes

Slide 8

Slide 8 text

Entry Point (Android) setContent { MaterialTheme { Text(text=“hello world”) } }

Slide 9

Slide 9 text

setContent {} Root Composition

Slide 10

Slide 10 text

setContent { // houses ui- building blocks such as Box, Lazy Layout, Text etc Text(text=“hello world”) }

Slide 11

Slide 11 text

@Composable fun HelloWorld():Unit Execution of this composable Leads to scheduling of changes Changes involve inserting, moving, replacing ui Nodes within the layout tree

Slide 12

Slide 12 text

Root Composable (setContent) HelloWorld Composable Text(“Hello World”) Layout Node Tree • Replaced • Removed • Inserted

Slide 13

Slide 13 text

Compose Ui-Building blocks, such as Box, Lazy Column, Text, Image etc are modelled as Layouts

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Layout Node contains a list of all its children

Slide 17

Slide 17 text

Layout Node is also responsible for inserting, removing, reordering its children Layout Node contains a list of all its children

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

*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

Slide 21

Slide 21 text

Tree Row Text() Text() Column Text() Owner

Slide 22

Slide 22 text

@Composable fun Box(modifier: Modifier) { Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier =modifier) } 😊 Example with the good Ol’ Box

Slide 23

Slide 23 text

How does a Composable emit Layout Node? Re-usable Compose Node Box Layout Node Composition

Slide 24

Slide 24 text

@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

Slide 25

Slide 25 text

// emits the ReusableComposeNode ReusableComposeNode>( factory = { LayoutNode() }, update = { set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) set(viewConfiguration, ComposeUiNode.SetViewConfiguration) }, skippableUpdate = materializerOf(modifier), content = content ) }

Slide 26

Slide 26 text

Why ReusableComposeNode • These UI-Nodes have keys that identify them

Slide 27

Slide 27 text

• When the Key changes, the content of the Reusable compose node is updated • These UI-Nodes have keys that identify them Why ReusableComposeNode

Slide 28

Slide 28 text

• 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

Slide 29

Slide 29 text

• 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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

ReusableComposeNode>( factory = { LayoutNode() }, }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Turning the Layout Nodes making up the Node Tree to the actual UI ⭐

Slide 34

Slide 34 text

• 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

Slide 35

Slide 35 text

• 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

Slide 36

Slide 36 text

Applier UI Applier Runtime Android Implementation Of the Applier Compose Ui Abstract Applier

Slide 37

Slide 37 text

• 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

Slide 38

Slide 38 text

• 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

Slide 39

Slide 39 text

When a node is visited, the node is pushed/inserted into the stack

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

@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”) } }}

Slide 43

Slide 43 text

Root (setContent{}) Column Text Text Tree

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

The popping and insertion of nodes into the Stack are carried out by the Abstract Applier

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Strategy used by Appliers to build the tree Top-down approach Bottom-up approach

Slide 50

Slide 50 text

Bottom up Approach Nodes are inserted bottom –up, to avoid duplicate notification of ancestral nodes Row

Slide 51

Slide 51 text

Row Text Text Inserting the two texts composables Notifies the Row composable only,

Slide 52

Slide 52 text

Row Text Column Text

Slide 53

Slide 53 text

What does this mean performance wise?

Slide 54

Slide 54 text

Nesting of composables in Jetpack compose is not a problem because of the bottom-up approach used By the Ui Applier

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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) }

Slide 57

Slide 57 text

Top Down Approach Building of the Ui tree starts from top to down, This approach is used by Vector Appliers Row Text Text

Slide 58

Slide 58 text

In top-down approach, during node insertion, all ancestors are notified

Slide 59

Slide 59 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

Slide 60

Slide 60 text

We know how to build the Ui Tree but how do we exactly do we draw the actual Ui?

Slide 61

Slide 61 text

Owner (AndroidComposeView) Jetpack compose is just a UI framework, independent of its implementations.

Slide 62

Slide 62 text

Existing implementations of Jetpack compose, thus must have an integration point Owner (AndroidComposeView) Jetpack compose is just a UI framework, independent of its implementations.

Slide 63

Slide 63 text

Jetpack compose Platform [Android] AndroidComposeView 🤝 Integration Point

Slide 64

Slide 64 text

The Android compose view connects/bridges the Layout Nodes with Android Views

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Whenever a node is added, updated or removed from the tree, the Android compose view Invalidates itself

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

“This is Greek, give me examples!!”

Slide 70

Slide 70 text

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☺ */

Slide 71

Slide 71 text

New Node List 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”

Slide 72

Slide 72 text

Owner [AndroidComposeView] calls invalidate / requestLayout() Owner [AndroidComposeView] calls dispatchDraw method Owner [AndroidComposeView] calls onMeasure()

Slide 73

Slide 73 text

Root Layout Node draw() method is invoked, Layout Nodes of the tree are drawn

Slide 74

Slide 74 text

Layout Node Measured Laid out Drawn Per ordinem/ order of things

Slide 75

Slide 75 text

“Yes precious it is making sense now!”

Slide 76

Slide 76 text

*Layout Nodes are drawn according to their zIndex

Slide 77

Slide 77 text

*Process for removing/inserting Layout Nodes follows a similar procedure *Layout Nodes are drawn according to their zIndex

Slide 78

Slide 78 text

*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

Slide 79

Slide 79 text

The end