Compose glance at

1. Development tools

Since the official version of Compose has not been released yet, you need to download the latest Canary version of Android Studio preview to enjoy the preview mode of Compose in advance.

2. Hello World

Start by creating an Empty Jetpack Compose project. Open the Android Studio preview of the latest Version of Canary, select the Empty Compose Activity template, and start the Compose journey:

When we created the Jetpack Compose project, we realized that API 21: Android 5.0 was the Minimum option for the Minimum SDK, which means that the Minimum version that Jetpack Compose supports is API 21: Android 5.0.

Click Finish to complete the project build. When the build is complete, it will look like this:

Red box 1: Toggle code and layout styles “A successful build is needed before the preview can be displayed” click on “Build & Refresh…”

Wait until the project is built, but the effect does not appear, at which point we need to run the project or click “Build & Refresh…” Or we can Build the project ourselves manually to get the final result.

Red box 1. Refresh the view. When the code changes, click on it to refresh the view (first need to Build). Deploy the current view directly to the device to see the effect. More about layout preview tool use, can view the official website of the introduction of the content developer. The android, Google. Cn/jetpack/com…

For Compose, I just want to manually configure it myself

The Compose configuration is automatically configured by the Studio tool when the Empty Compose Activity template is created. What is the difference between the Compose project and the normal project?

  1. Jetpack Compose is built around Kotlin. In some cases, Kotlin provides special idioms that make it easier to write good Compose code. If your project does not yet support it, check out the tutorial to add Kotlin to your existing app configuration (Kotlin needs 1.4 +).

  2. The configuration for Jetpack Compose is in your project’s app/build.gradle file:


plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        ...
        
        // Jetpack Compose minimum supported version
        minSdk 21
    }

    // Based on Jdk version 1.8compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions  { jvmTarget ='1.8'
    }
    
    
    buildFeatures {
        // Enable Jetpack Compose
        compose true
    }
    composeOptions {
        / / compose_version = '1.0.0 - beta09'
        kotlinCompilerExtensionVersion compose_version
    }
}

dependencies {

    implementation 'androidx. Core: the core - KTX: 1.3.2'
    implementation 'androidx. Appcompat: appcompat: 1.2.0'
    implementation 'com. Google. Android. Material: material: 1.3.0'
    For example, 'androidx' is composed of 6 Maven group ids. Each group contains a set of functions for a specific purpose
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation 'androidx. Lifecycle: lifecycle - runtime - KTX: 2.3.1'
    implementation 'androidx. Activity: activity - compose: 1.3.0 - alpha06'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx. Test. Ext: junit: 1.1.2'
    androidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.3.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
}

Copy the code

“Compose” is composed of 6 Maven group ids in AndroidX. Each group contains a set of functions for a specific purpose

group instructions
compose.animation Build animations in the Jetpack Compose app to enrich the user experience.
compose.compiler Transform @Composable Functions and enable optimization with the Kotlin compiler plug-in.
compose.foundation Write the Jetpack Compose application using off-the-shelf building blocks, and you can also extend Foundation to build your own design system elements.
compose.material Build the Jetpack Compose UI using off-the-shelf Material Design components. This is the entry point for higher-level Compose, which is designed to provide access towww.material.ioComponents that are consistent with those described above.
compose.runtime The basic building blocks for Compose’s programming model and state management, as well as the core runtime targeted by the Compose compiler plug-in.
compose.ui The basic components of the Compose UI required to interact with the device, including layout, drawing, and input.

3. Composable functions

Whereas we used to write page views in XMl, Jetpack Compose is built around composable functions. These functions allow you to programmatically define an application interface by describing the shape and data dependencies of the application interface rather than the process of building the interface. To create a Composable function, simply add the @Composable annotation to the function name.


setContent {
    // Use the "background" color of the surface container in the theme (set the background, with Material Design properties)
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

@Composable 
fun Greeting(name: String) { 
    Text(text = "Hello $name!")}Copy the code

The setContent (not setContentView) block defines the Activity’s layout. Instead of using XML files to define the layout content, we call composable functions. Jetpack Compose uses the custom Kotlin compiler plug-in to convert these composable functions into interface elements for your application. For example, the Compose interface library defines the Text() function; You can call this function to declare text elements in your application.

A composable function can only be called within the scope of other composable functions. To make the function Composable, add a @Composable annotation. To try this, define a Greeting() function and pass it a name, which the function then uses to configure the text element.

The current Version of Android Studio, Canary, allows you to preview composable functions in the IDE without having to download the app to an Android device or emulator. The main limitation is that a combinable function cannot accept any arguments. Therefore, you cannot preview the Greeting() function directly. Instead, you need to create another function called PreviewGreeting(), which calls Greeting() with the appropriate arguments. Please add the @Preview comment above the @Composable.

Learn about the first component, Text

@Composable
fun Text(
    text: String./ / text
    // Modifier an ordered, immutable collection of Modifier elements used to add decorations or behavior to the Compose UI element. Examples include background, padding, click events, etc.
    modifier: Modifier = Modifier, 
    color: Color = Color.Unspecified, // Text color
    fontSize: TextUnit = TextUnit.Unspecified, // The text size
    fontStyle: FontStyle? = null.// The font variant used to draw letters (for example, italics)
    fontWeight: FontWeight? = null.// Font size
    fontFamily: FontFamily? = null.// The font family to use when rendering text
    letterSpacing: TextUnit = TextUnit.Unspecified, / / word spacing
    textDecoration: TextDecoration? = null.// Text decorations, such as underscores
    textAlign: TextAlign? = null.// Alignment
    lineHeight: TextUnit = TextUnit.Unspecified, / / line height
    overflow: TextOverflow = TextOverflow.Clip, // The end of the text display processing, such as the tail display... Or in the middle...
    softWrap: Boolean = true.// Whether the text should break at the newline. If false, the width of the text extends horizontally indefinitely, and the textAlign attribute is invalid, and an exception may occur.
    maxLines: Int = Int.MAX_VALUE, // Maximum number of rows
    onTextLayout: (TextLayoutResult) -> Unit = {}, // The callback to perform when calculating the new text layout
    style: TextStyle = LocalTextStyle.current // Styles of the text, such as color, font, and line height. This means that some of the above properties, such as color and fontSize, can also be declared here. You can refer to the TextStyle class for the contained properties.
){... }Copy the code

Take a look at one of the advantages of our declarative data update

mutableStateOf

class MainActivity : ComponentActivity(a){
    // Change the inputText type to mutableState, indicating that the data is stateful. If this changes, all components designed to be redrawn
    var inputText = mutableStateOf("")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // var inputText by mutableStateOf("") in the Compose field
            Content()
        }
    }
 
    @Composable
    fun HelloContent() {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Hello",
                modifier = Modifier.padding(bottom = 8.dp)
            )
            OutlinedTextField( / / the EditText analogy
                value = inputText.value,
                onValueChange = { inputText.value = it },
                label = { Text(text = "Name")},)}}}Copy the code

MutableStateOf holds the state, inputText is a class member variable, and JetPack Compose is a “functional” type of programming, so it is usually impossible or inconvenient to reference a class variable. Remember is needed.

Note: Var inputText by mutableStateOf(“”) can also be placed on a component’s parent or grandfather. Component, but the prerequisite is to ensure that the change causes the interface to be redrawn by its parent or grandfather. Components will not be redrawn if their parent or grandfather.. The component will be redrawn, and the value will be reinitialized during the redrawing, causing the change to fail. Remember is needed to cache the value or save it in the ViewModel.

Remember and ViewModel


// 1. Remember: "Remember stores values, and when the interface is redrawn, the previously stored values are read."
@Composable
fun Content() {
    val inputText = remember { mutableStateOf("")}Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello",
            modifier = Modifier.padding(bottom = 8.dp)
        )
        OutlinedTextField(
            value = inputText.value,
            onValueChange = { inputText.value = it },
            label = { Text(text = "Name")},)}}// 2. ViewModel
class WeViewModel : ViewModel(a){
    var inputText by mutableStateOf("")
}

@Composable
fun Content() {
    / / need to rely on implementation 'androidx. Lifecycle: lifecycle - viewmodel - compose: 1.0.0 - alpha07'

    val viewModel: WeViewModel = viewModel()
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello",
            modifier = Modifier.padding(bottom = 8.dp)
        )
        OutlinedTextField(
            value = viewModel.inputText.value,
            onValueChange = { viewModel.inputText.value = it },
            label = { Text(text = "Name")},)}}Copy the code

Compose analyzes code blocks that are affected by a state change at compile time and records references to them. When the state changes, it finds these code blocks by reference and marks them as Invalid. Compose triggers recomposition before the next rendered frame arrives and executes an invalid block during the recomposition process.

# Learn about Compose’s reorganization scope

Compose base component

For example: Compose a magical and “disgusting” Modifier

Modifier Modifier

With modifiers, you can modify or extend composable items. You can use modifiers to do the following:

  • Changes the size, layout, behavior, and appearance of the composable items
  • Add information such as accessibility labels
  • Processing user input
  • Add advanced interactions, such as making elements clickable, scrollable, draggable, or zooming

Modifiers are standard Kotlin objects. You can create modifiers by calling an Modifier class function. You can tie together the following functions to combine them:

@Composable
fun ArtistCard(artist: Artist, onClick: () -> Unit) {
    val padding = 16.dp
    Column(Modifier .clickable(onClick = onClick) .padding(padding) .fillMaxWidth()) {
        Row(verticalAlignment = Alignment.CenterVertically) { / *... * / }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { / *... * /}}}Copy the code

Notice that in the code above, different modifier functions are used in combination.

  • clickableCauses composable items to respond to user input and display ripples.
  • paddingLeave space around the element.
  • fillMaxWidthCauses the composable item to fill in the maximum width provided for it by its parent.
  • size()Specifies the preferred width and height of the element.

Note: Modifiers act like layout parameters in view-based layouts, among other things. However, because modifiers are sometimes specific to a particular scope, they not only ensure type safety, but also help you discover and understand the elements that are available and applicable to a layout. With XML layouts, it is sometimes difficult to find out whether specific layout attributes apply to a given view.

The order of modifiers is important

The order of the modifier functions is important. Since each function changes the Modifier returned by the previous function, the order affects the final result. Let’s look at an example of this:

@Composable
fun ArtistCard(/ *... * /) {
    val padding = 16.dp
    Column(Modifier .clickable(onClick = onClick) .padding(padding) .fillMaxWidth()) {
        // rest of the implementation}}Copy the code

In the above code, the entire area (including the surrounding inner margins) is clickable because the padding modifier is applied after the Clickable modifier. ** If the order of modifiers is reversed, the space added by padding will not respond to user input

Note: The explicit order helps you infer how the different modifiers will interact. You can compare this to a view-based system. In view-based systems, you must understand the box model, in which margins are applied “outside” and “inside” elements, and background elements are resized accordingly. The decorator design makes this behavior explicit and predictable, and gives you more control to achieve the exact behavior you expect. This also explains why there is no margin modifier, only padding.

Built-in modifier

Jetpack Compose provides a list of built-in modifiers to help you decorate or extend composable items. Modifiers such as PADDING, Clickable, and fillMaxWidth have been introduced.

Compose Modifier: Compose Modifier

For example: Compose itself

  1. Canves: graphics in Compose
  2. Layout: custom Layout

Compose natural characteristic

Compose a measurement of inherent properties in the layout

For example, Compose

Question:

  1. How is setContent set@ComposableComponent to initialize the view?
  2. @ComposableHow do you implement a series of views through functions?
  3. How does the Modifier we set work?
  4. Where does the measurement process begin? How is it measured?
  5. Where is the draw entry?

The answer:

  1. The setContent method initializes the content view by creating a ComposeView and setting it as r.Layout. XXX in the original setContentView method.
  2. @ComposableFunction finally generates a LayoutNode, some columns@ComposableThe result is a LayoutNode tree with root as the root (similar to View🌲).
  3. The Modifier is bound to a LayoutNode, and each Modifier’s extension method first generates a chain of combinedModifiers through the THEN method, That is, each CombinedModifier contains the Modifier returned by the previous extension method, which is then converted to a LayoutNodeWrapper chain in LayoutNode, Then, during the measurement process, we recursively traverse all the LayoutNodeWrappers in the chain to measure each property. Finally, it goes back to the last node InnerPlaceable in the LayoutNodeWrapper chain (initialized in the LayoutNode member property) to measure the current LayoutNode child and record the result.
  4. The measurement process starts with our AndroidComposeView onMeasure method, recursively measuring all LayoutNodes through the Root in AndroidComposeView, Each LayoutNode is measured by the LayoutNodeWrapper chain.
  5. In AndroidComposeView, call root.draw(this) in dispatchDraw method

View initialization –setContent

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null.content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if(existingComposeView ! =null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        / / 1.
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

//
fun setContent(content: @Composable () -> Unit) {
    shouldCreateCompositionOnAttachedToWindow = true
    this.content.value = content
    if (isAttachedToWindow) {
        / / 2.
        createComposition()
    }
}

// the code at ②
fun createComposition() {
    check(parentContext ! =null || isAttachedToWindow) {
        "createComposition requires either a parent reference or the View to be attached" +
                "to a window. Attach the View or call setParentCompositionReference."
    }
    / / 3.
    ensureCompositionCreated()
}

// the code at ③
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
private fun ensureCompositionCreated() {
    if (composition == null) {
        try {
            creatingComposition = true
            / / 4.
            composition = setContent(resolveParentCompositionContext()) {
                Content()
            }
        } finally {
            creatingComposition = false}}}// the code at the end
internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    AndroidComposeView is initialized and addView is added to the ComposeView
    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
    / / 6.
    return doSetContent(composeView, parent, content)
}
Copy the code

Comb through:

  1. In setContent we see the creation of a ComposeView(ViewGroup), then we see the familiar setContentView and set the layout of the interface as we used to do with r.Layout.xxx
  2. AndroidComposeView(ViewGroup) is initialized in the ⑤ position, and the addView is added to the ComposeView. AndroidComposeView is the last of the familiar viewgroups. Measurement, layout, and drawing are all enabled by AndroidComposeView.

The last destination for the @composable method –LayoutNode

We use a series of @Composable functions to display the interface. How do @Composable functions implement a series of views? The answer is LayoutNode

LayoutNode’s past and present lives

@Suppress("ComposableLambdaParameterPosition")
@Composable inline fun Layout(content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        // Initialize the LayoutNode
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
        },
        // ② Bind the modifierskippableUpdate = materializerOf(modifier), Content = content)} = = = = = = = = = = = = = = = = = = = = = = = = = = left left left left left down down down down = = = = = = = = = = = = = = = = = = = = = = = = = = = = = @ Composable @ExplicitGroupsComposable inline fun <T, reified E : Applier<*>>ReusableComposeNode(noinline factory: () -> T, update: @DisallowComposableCalls Updater
       
        .() -> Unit, noinline skippableUpdate: @Composable SkippableUpdater
        
         .() -> Unit, content: @Composable () -> Unit
        ) {
    if(currentComposer.applier ! is E) invalidApplier() currentComposer.startReusableNode()// This is true
    if (currentComposer.inserting) {
        // Create a new Node
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    currentComposer.disableReusing()
    // Call back to update
    Updater<T>(currentComposer).update()
    currentComposer.enableReusing()
    // ③ Callback skippableUpdate
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    // ✋🏻 calls the @composable annotation method
    content()
    currentComposer.endReplaceableGroup()
    // ④ End NodeCurrentComposer. EndNode ()} = = = = = = = = = = = = = = = = = = = = = = = = = = left left to look at the first create node related left left = = = = = = = = = = = = = = = = = = = = = = = = = = = @ Suppress ("UNUSED")
override fun <T> createNode(factory: () -> T) {
    validateNodeExpected()
    check(inserting) { "createNode() can only be called when inserting" }
    val insertIndex = nodeIndexStack.peek()
    val groupAnchor = writer.anchor(writer.parent)
    groupNodeCount++
    // record 1
    recordFixup { applier, slots, _ ->
        @Suppress("UNCHECKED_CAST")
        val node = factory()
        slots.updateNode(groupAnchor, node)
        @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
        // If the nodeApplier is UiApplier, then this method is empty
        nodeApplier.insertTopDown(insertIndex, node)
        // ⑥ Reconfigure the Current LayoutNode to prepare for subsequent binding of the Modifier
        applier.down(node)
    }
  
    // ⑦ Record 2
    recordInsertUpFixup { applier, slots, _ ->
        @Suppress("UNCHECKED_CAST")
        val nodeToInsert = slots.node(groupAnchor)
        }}}}}}}}}}}}}}}}}}}}}}}}}}}
        applier.up()
        @Suppress("UNCHECKED_CAST") val nodeApplier = applier asApplier<Any? >// If the nodeApplier is UiApplier, insert the node using this method,
        nodeApplier.insertBottomUp(insertIndex, nodeToInsert)
    }
}
  
private val insertFixups = mutableListOf<Change>()
  
// * /
private fun recordFixup(change: Change) {
    insertFixups.add(change)
}

private val insertUpFixups = Stack<Change>()
  
// * /
private fun recordInsertUpFixup(change: Change) {
    insertUpFixups.push(change)
}

// Add Change from ⑦ to ⑤
private fun registerInsertUpFixup() {
    insertFixups.add(insertUpFixups.pop())
}

// ⑨ This method is called on the end Node (④) above
private fun recordInsert(anchor: Anchor) {
    if (insertFixups.isEmpty()) {
        val insertTable = insertTable
        recordSlotEditingOperation { _, slots, _ ->
            slots.beginInsert()
            slots.moveFrom(insertTable, anchor.toIndexFor(insertTable))
            slots.endInsert()
        }
    } else {
        // Process all changes that ⑤ add into insertFixups only logicval fixups = insertFixups.toMutableList() insertFixups.clear() realizeUps() realizeDowns() val insertTable = insertTable  recordSlotEditingOperation { applier, slots, rememberManager -> insertTable.write { writer -> fixups.fastForEach { fixup ->// Call back the lamdba in ⑤fixup(applier, writer, rememberManager) } } slots.beginInsert() slots.moveFrom(insertTable, Anchor. ToIndexFor (insertTable) slots. EndInsert ()}}} = = = = = = = = = = = = = = = = = = = = = = left still looks left (3) left left modifier was introduced into the method ======================= @PublishedApi internal fun materializerOf( modifier: Modifier ): @Composable SkippableUpdater<ComposeUiNode>.() -> Unit = { val materialized = currentComposer.materialize(modifier) update {// The body of the set method is at the bottom
        set(materialized, ComposeUiNode.SetModifier)
    }
}

@Suppress("INLINE_CLASS_DEPRECATED"."EXPERIMENTAL_FEATURE_WARNING")
inline class SkippableUpdater<T> constructor(@PublishedApi internal val composer: Composer
) {
    inline fun update(block: Updater<T>.() -> Unit) {
        composer.startReplaceableGroup(0x1e65194f)
        . / / block, here is the last method ComposeUiNode SetModifier lamdba, namely below the full version
        Updater<T>(composer).block()
        composer.endReplaceableGroup()
    }
}

// Set method body
fun <V> set(
    value: V,
    block: T.(value: V) -> Unit
) = with(composer) {
    if(inserting || rememberedValue() ! = value) { updateRememberedValue(value)// Apply encapsulates the operation as Change for subsequent execution
        composer.apply(value, block)
    }
}

override fun <V, T> apply(value: V, block: T.(V) -> Unit) {
    // Apply encapsulates the operation as Change for subsequent execution
    val operation: Change = { applier, _, _ ->
        @Suppress("UNCHECKED_CAST")
        (applier.current as T).block(value)
    }
    if (inserting) recordFixup(operation)
    else recordApplierOperation(operation)
}

/ / ComposeUiNode SetModifier full version, this is this LayoutNode, now binding modifier
val SetModifier: ComposeUiNode.(Modifier) -> Unit = { this. Modifier it} = = = = = = = = = = = = = = = = = = = = = = = = left left at this point (4) endNode executed left left = = = = = = = = = = = = = = = = = = = = = = = = override fun endNode = () end(isNode =true)

private fun end(isNode: Boolean){... val inserting = insertingif (inserting) {
        if (isNode) {
            // 🔟 Add Change in ⑥ to ⑤
            registerInsertUpFixup()
            expectedNodeCount = 1
        }
        reader.endEmpty()
        val parentGroup = writer.parent
        writer.endGroup()
        if(! reader.inEmpty) { val virtualIndex = insertedGroupVirtualIndex(parentGroup) writer.endInsert() writer.close()// Call code ⑨, which handles the unique logic of all changes in insertFixups, and turn on the callback at create node and ③
            recordInsert(insertAnchor)
            this.inserting = false
            if(! slotTable.isEmpty) { updateNodeCount(virtualIndex,0)
                updateNodeCountOverrides(virtualIndex, expectedNodeCount)
            }
        }
    } else {
        if (isNode) recordUp()
        recordEndGroup()
        val parentGroup = reader.parent
        val parentNodeCount = updatedNodeCount(parentGroup)
        if(expectedNodeCount ! = parentNodeCount) { updateNodeCountOverrides(parentGroup, expectedNodeCount) }if (isNode) {
            expectedNodeCount = 1} reader.endGroup() realizeMovement() } exitGroup(expectedNodeCount, Inserting)} = = = = = = = = = = = = = = = = = = = = = = = left left at 6 and was left before and after the execution left = = = = = = = = = = = = = = = = = = = = = = = = private val stack = mutableListOf < T > () overridevar current: T = root
    protected set

override fun down(node: T) {
    // 
    stack.add(current)
    current = node
}

override fun up() {
    check(stack.isNotEmpty())
    current = stack.removeAt(stack.size - 1)
}
    
override fun insertTopDown(index: Int, instance: LayoutNode) {
    / / ignore. Insert in [insertBottomUp] to build the tree from bottom up to avoid repeated notifications when child nodes enter the tree.
} 

// Insert the node
override fun insertBottomUp(index: Int, instance: LayoutNode) {
    current.insertAt(index, instance)
}
Copy the code

A little comb:

  1. ReusableComposeNode enables node creation and parameter Settings, such as Factory, Update, and skippableUpdate.
  2. The factory, update, and skippableUpdate operations are all changed to Change for subsequent execution.
  3. After currentComposer.endNode() finishes, it starts to iterate through all the changes in order.

Let’s now focus on ✋🏻 between ③ and ④ and implement our method using the @composable annotation:

  1. (4) insert the current node into the current node

  2. Let’s focus on the node code at 🔟, which first puts all the up and insertBottomUp operations at ⑦ into all the down and insertTopDown operations at ⑤.

  3. After all down and insertTopDown operations have been performed, all layoutNodes have content from root(the outermost layer) to the innermost layer: @composable ****ColumnScope.() -> LayoutNode for the Unit argument is stored in the stack cache stack.

  4. After the up and insertBottomUp operations are performed, a complete LayoutNode tree is constructed by fetching layoutNodes from the stack backwards.

  5. The full LayoutNode tree, all the leaf nodes, is without content: @composable ****ColumnScope.() -> LayoutNode for the @composable parameter. Root is root and the rest of the pages are down.

Confusing operations for applier.down(node) and applier.up()?

// record 1
recordFixup { applier, slots, _ ->
    @Suppress("UNCHECKED_CAST")
    val node = factory()
    slots.updateNode(groupAnchor, node)
    @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
    // If the nodeApplier is UiApplier, then this method is empty
    nodeApplier.insertTopDown(insertIndex, node)
    // ⑥ Reconfigure the Current LayoutNode to prepare for subsequent binding of the Modifier
    applier.down(node)
}

// ⑦ Record 2
recordInsertUpFixup { applier, slots, _ ->
    @Suppress("UNCHECKED_CAST")
    val nodeToInsert = slots.node(groupAnchor)
    }}}}}}}}}}}}}}}}}}}}}}}}}}}
    applier.up()
    @Suppress("UNCHECKED_CAST") val nodeApplier = applier asApplier<Any? >// If the nodeApplier is UiApplier, insert the node using this method,
    nodeApplier.insertBottomUp(insertIndex, nodeToInsert)
}
Copy the code

Cause: Change is added in the order createNode, Update, skippableUpdate, and traversal needs to be in the same order.

Applier.down (node) first sets the createNode to the current node. Current (applier.current as T).block(value) in update, skippableUpdate (applier.current as T). At this point, the LayoutNode and Modifier are bound successfully. The subsequent applier.up() is used to insert the createNode into its parent node. The createNode cannot be used as current because the parent node may have multiple children and its siblings need to be inserted into the parent node.

Why is applier UiApplier? Who is the root LayoutNode?

"Undertake [View initialization --serContent] part of the source code"

// ⑥ code
@OptIn(InternalComposeApi::class)
private fun doSetContent(
    owner: AndroidComposeView.parent: CompositionContext.content: @Composable() - >Unit) :Composition {
    if (inspectionWanted(owner)) {
        owner.setTag(
            R.id.inspection_slot_table_set,
            Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
        )
        enableDebugInspectorInfo()
    }
    // ⑦ Initialize Composition and UiApplier
    val original = Composition(UiApplier(owner.root), parent)
    val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
        as? WrappedComposition ? : WrappedComposition(owner, original).also { owner.view.setTag(R.id.wrapped_composition_tag, it) } wrapped.setContent(content)return wrapped
}

// initialize UiApplier
internal class UiApplier(
    root: LayoutNode
) : AbstractApplier<LayoutNode> (root) {... }AbstractApplier; current = root; owner.root
abstract class AbstractApplier<T> (val root: T) : Applier<T> {
    private val stack = mutableListOf<T>()
    override var current: T = root
        protected set
   
    ......
}
Copy the code

We initialize UiApplier, and we pass in an owner.root parameter. The owner is actually AndroidComposeView. The root LayoutNode is the owner.root member variable in the AndroidComposeView.

There are only three UiApplier classes, VectorApplier, and AbstractApplier classes. The first two inherit from the last abstract class. VectorApplier is only used in VectorComponent and VectorPainter.

Summary: @composable annotation methods are finally built into the LayoutNode insertAt into the AndroidComposeView root, so the final measurement and layout starts in the AndroidComposeView.

Jetpack Compose measurement process

We know from the above that the root LayoutNode is in the AndroidComposeView, so the LayoutNode measurement must also start in the AndroidComposeView, Let’s start with the AndroidComposeView onMeasure method:

override val root = LayoutNode().also {
    it.measurePolicy = RootMeasurePolicy
    it.modifier = Modifier
        .then(semanticsModifier)
        .then(_focusManager.modifier)
        .then(keyInputModifier)
}

private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    trace("AndroidOwner:onMeasure"){... val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec) val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec) val constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight) ...// update constraints and status
        measureAndLayoutDelegate.updateRootConstraints(constraints)
        // ② Measurement and layoutmeasureAndLayoutDelegate.measureAndLayout() setMeasuredDimension(root.width, root.height) ... }} = = = = = = = = = = = = = = = = = = = = = = = = = = 1) update the state constraints and = = = = = = = = = = = = = = = = = = = = = = = = = = = = = funupdateRootConstraints(constraints: Constraints) {
    if(rootConstraints ! = constraints) {require(! duringMeasureLayout) rootConstraints = constraints// Update the root status
        root.layoutState = LayoutNode.LayoutState.NeedsRemeasure
        // Add root to the layout node that you want to measure or layoutRelayoutNodes. Add (root)}} = = = = = = = = = = = = = = = = = = = = = = = = = = (2) measurement and layout = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = fun measureAndLayout():Boolean {
    ...
    val rootConstraints = rootConstraints ?: return false

    var rootNodeResized = false
    if (relayoutNodes.isNotEmpty()) {
        duringMeasureLayout = true
        try {
            // Remove the root that you just added
            relayoutNodes.popEach { layoutNode ->
                if (layoutNode.isPlaced ||
                    layoutNode.canAffectParent ||
                    layoutNode.alignmentLines.required
                ) {
                    // In (1), the status is changed to NeedsRemeasure
                    if (layoutNode.layoutState == LayoutNode.LayoutState.NeedsRemeasure) {
                        // 3
                        if (doRemeasure(layoutNode, rootConstraints)) {
                            rootNodeResized = true}}... }}}finally {
            duringMeasureLayout = false} consistencyChecker? .assertConsistent() }returnRootNodeResized} = = = = = = = = = = = = = = = = = = = = = = = = = = (3) to measure the = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = private fun doRemeasure(layoutNode: LayoutNode,rootConstraints: Constraints): Boolean {
    // The current layoutNode is root
    val sizeChanged = if (layoutNode === root) {
        // Start measuring
        layoutNode.remeasure(rootConstraints)
    } else {
        layoutNode.remeasure()
    }
    val parent = layoutNode.parent
    if (sizeChanged) {
        if (parent == null) {
            return true
        } else if (layoutNode.measuredByParent == LayoutNode.UsageByParent.InMeasureBlock) {
            requestRemeasure(parent)
        } else {
            require(layoutNode.measuredByParent == LayoutNode.UsageByParent.InLayoutBlock)
            requestRelayout(parent)
        }
    }
    return false
}

/ / root LayoutNode root
internal fun remeasure(
    constraints: Constraints = outerMeasurablePlaceable.lastConstraints
) = outerMeasurablePlaceable.remeasure(constraints)

// OuterMeasurablePlaceable
fun remeasure(constraints: Constraints): Boolean{...// In this case layoutState == NeedsRemeasure
    if(layoutNode.layoutState == LayoutNode.LayoutState.NeedsRemeasure || measurementConstraints ! = constraints ) { layoutNode.alignmentLines.usedByModifierMeasurement =false
        layoutNode._children.forEach { it.alignmentLines.usedDuringParentMeasurement = false }
        measuredOnce = true
        layoutNode.layoutState = LayoutNode.LayoutState.Measuring
        measurementConstraints = constraints
        val outerWrapperPreviousMeasuredSize = outerWrapper.size
        owner.snapshotObserver.observeMeasureSnapshotReads(layoutNode) {
            // ⑤ The LayoutNodeWrapper chain starts measuring entry
            outerWrapper.measure(constraints)
        }
        ......
        return sizeChanged
    }
    return false} = = = = = = = = = = = = = = = = = = 5 LayoutNodeWrapper chain began measuring entrance = = = = = = = = = = = = = = = = = = = = = internalclass OuterMeasurablePlaceable(
    private val layoutNode: LayoutNode.var outerWrapper: LayoutNodeWrapper // outerWrapperConstruct incoming) //LayoutNodeIn the initializationOuterMeasurablePlaceable
internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
    private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
Copy the code

Finally, the measure of root’s innerLayoutNodeWrapper is called to measure the entire LayoutNodeWrapper chain

OuterWrapper who is it? What is the LayoutNodeWrapper chain?

A: in [LayoutNode’s past life] ② bind the modifier, and finally actually call LayoutNode’s modifier member attribute

Set method:

internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)

private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)

override var modifier: Modifier = Modifier
    set(value) {
        if (value == field) return
        if(modifier ! = Modifier) {require(! isVirtual) { "Modifiers are not supported on virtual LayoutNodes" }
        }
        field = value

        ...
        / / (1) modifier. FoldOut
        val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
            var wrapper = toWrap
            if (mod is RemeasurementModifier) {
                mod.onRemeasurementAvailable(this)
            }

            val delegate = reuseLayoutNodeWrapper(mod, toWrap)
            if(delegate ! =null) {
                if (delegate is OnGloballyPositionedModifierWrapper) {
                    getOrCreateOnPositionedCallbacks() += delegate
                }
                wrapper = delegate
            } else {
                // ② By judging the type of Modifier, each Modifier operation is wrapped into the LayoutNodeWrapper
                if (mod is DrawModifier) {
                    wrapper = ModifiedDrawNode(wrapper, mod)
                }
                if (mod is FocusModifier) {
                    wrapper = ModifiedFocusNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is FocusEventModifier) {
                    wrapper = ModifiedFocusEventNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is FocusRequesterModifier) {
                    wrapper = ModifiedFocusRequesterNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is FocusOrderModifier) {
                    wrapper = ModifiedFocusOrderNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is KeyInputModifier) {
                    wrapper = ModifiedKeyInputNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is PointerInputModifier) {
                    wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
                }
                if (mod is NestedScrollModifier) {
                    wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
                }
                if (mod is LayoutModifier) {
                    wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is ParentDataModifier) {
                    wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)
                }
                if (mod is SemanticsModifier) {
                    wrapper = SemanticsWrapper(wrapper, mod).assignChained(toWrap)
                }
                if (mod is OnRemeasuredModifier) {
                    wrapper = RemeasureModifierWrapper(wrapper, mod).assignChained(toWrap)
                }
                if(mod is OnGloballyPositionedModifier) { wrapper = OnGloballyPositionedModifierWrapper(wrapper, mod).assignChained(toWrap) getOrCreateOnPositionedCallbacks() += wrapper } } wrapper } outerWrapper.wrappedBy = parent? .innerLayoutNodeWrapper outerMeasurablePlaceable.outerWrapper = outerWrapper ...... }// This higher-order function is not easy to understand
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
        outer.foldOut(inner.foldOut(initial, operation), operation)

// Let's switch a little bit
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R = {
    / / 3.
    val inn = inner.foldOut(initial, operation)
    outer.foldOut(inn , operation)
}   

// At the end of the recursion, the operation callback is called
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
    operation(this, initial)
Copy the code
  1. Through the foldOut function we can see, is actually recursively traversed all the Modifier operation, and then through the operation callback function to convert the current Modifier and initial values;

  2. Operation actually wraps the current modifier operation and the initial value (i.e. ③ the inner. FoldOut recursive value, LayoutNodeWrapper) into a new LayoutNodeWrapper (i.e. ② the logic;

  3. Since recursive traversal always wraps the current modifier operation and the initial value into a new LayoutNodeWrapper, which in turn is the return value of inden.foldout that is entered into the next recursion, So you end up with a LayoutNodeWrapper chain structure.

  4. Since the initial value is InnerPlaceable, the final node of the entire LayoutNodeWrapper chain is InnerPlaceable.

  5. The innerLayoutNodeWrapper of root actually ends up being the new LayoutNodeWrapper chain head of outerWrapper transformed by the foldOut function.