Jetpack Compose measurement process source code analysis

Posted by dman779 on Sun, 23 Jan 2022 20:40:02 +0100

What will you learn in this article

Through a code scenario, analyze the source code with the Layout function as the entry, and answer some questions. For example, if setting the size through the Modifier works? How is the measure method of the MeasurePolicy interface called? What is the measurement process in the Layout? How does the control confirm its size?

review

stay JetPack Compose handwritten a Row layout | custom layout In this article, we have learned how to customize Layout. You can use the Layout function.

@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {}
   

We can specify the size of the Layout through the parameter modifier, measure and arrange the children in the measurePolicy, and write the children of the Layout in the content function. It's very convenient to use 😁 , But curiosity makes me wonder what's done in this Layout? 🤔 The source code may be very complex, but I still want to try to have a look. How can I know if I don't try. 🚀

In order to facilitate the exploration and debugging of the code, this paper takes the following code as the scene for analysis.

@Composable
private fun ParentLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
  //Measurement strategy of layout
    val measurePolicy = MeasurePolicy { measurables, constraints ->
        //1. Measure children
        val placeables = measurables.map { child ->
            child.measure(constraints)
        }

        var xPosition = 0
        //2. Place children
        layout(constraints.minWidth, constraints.minHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(xPosition, 0)
                xPosition += placeable.width
            }
        }
    }
    //Code analysis portal
    Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}

@Composable
private fun ChildLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    //... The code is similar to ParentLayout
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
           ParentLayout(
                Modifier
                    .size(100.dp)
                    .padding(10.dp)
                    .background(Color.Blue)
            ) {
                ChildLayout {
                    Box {}
                }
                ChildLayout {}
            }
        }
    }
}

This exploration hopes to answer the following questions

  1. How does setting the size through modifier work in ParentLayout?
  2. How is the measure method of the MeasurePolicy interface called? How does his parameter value come from?
  3. What is the measurement process in the layout?

Let's try to explain these problems in the process of looking at the source code.

The corresponding version of the source code of this article is compose_version = ‘1.0.0-rc01’

Tracking modifier & measurepolicy

In order to track the code, I'll set up a tracker for the code (don't lose it) 😜
In the following code, I will use the position reached by the modifier parameter 📍 Mark the position reached by measurePolicy 📌 sign

Layout.kt → layout function source code

@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy) // 👈  📌  measurePolicy is here
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
        },
        skippableUpdate = materializerOf(modifier), // 👈  📍  modifier is here
        content = content 
    )
}

From the above source code, we can see that there is no processing in the Layout function body, and the core content is to call the ReusableComposeNode function.

@It is suggested that the functions annotated with Composable should be capitalized. Ordinary functions are distinguished. When looking at the code, you always think that ReusableComposeNode is a class. Click in and find that it is a Composable function 😂 .

Composables.kt → ReusableComposeNode function

inline fun <T, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    
    //Execute the update function
    Updater<T>(currentComposer).update() // 👈  📌  measurePolicy is here
    
     //Execute skippableUpdate function
    SkippableUpdater<T>(currentComposer).skippableUpdate()  // 👈  📍  modifier in this function
    
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

First, only those related to the modifier, that is, skippableupdater < T > (current composer) Skippableupdate(), don't look at the others.

The function parameters here are all function parameter types. If Kotlin is not familiar with them, they may look dizzy 😵 , Next, in order to facilitate the analysis, we flatten the code.

The "code flattens it" here means that the function callback and layer by layer calls are removed and written directly together to avoid function jumping around and facilitate interpretation. The flattening mentioned below means this.

Now I'll try to flatten it.

//source code
SkippableUpdater<T>(currentComposer).skippableUpdate() 

//0 ️⃣  According to the parameters passed in by ComposeNode
skippableUpdate=materializerOf(modifier) // 👈  📍 modifier 

//one ️⃣  The return value of the materializerOf function is the function type skippableupdater < composeuinode > () -> Unit
internal fun materializerOf(
    modifier: Modifier 
): @Composable SkippableUpdater<ComposeUiNode>.() -> Unit = {
     //  📍  Here, we just deal with the ComposedModifier existing in the modifier chain. The return value is still the modifier
    val materialized = currentComposer.materialize(modifier) 
    update { set(materialized, ComposeUiNode.SetModifier)  } // 👈  📍 modifier
}

//Combination code 0 ️⃣  And code 1 ️⃣  It can be seen that skippableupdater < T > (current composer) skippableUpdate() 
//< = > equivalent to the following code
val skippableUpdater=SkippableUpdater<ComposeUiNode>(currentComposer)
val materialized = currentComposer.materialize(modifier)
skippableUpdater.update { set(materialized, ComposeUiNode.SetModifier)  }

It seems that there's almost an update, but it's still not even

inline class SkippableUpdater<T> constructor(
    @PublishedApi internal val composer: Composer
) {
    inline fun update(block: Updater<T>.() -> Unit) {
        composer.startReplaceableGroup(0x1e65194f)
        Updater<T>(composer).block()
        composer.endReplaceableGroup()
    }
}

//Combined with the update function of SkippableUpdater, SkippableUpdater update { set(materialized, ComposeUiNode.SetModifier) }
// < = > equivalent to 👇
composer.startReplaceableGroup(0x1e65194f)
// two ️⃣ 📍  The modifier is finally passed to the set method
Updater<ComposeUiNode>(currentComposer).set(materialized, ComposeUiNode.SetModifier)
composer.endReplaceableGroup()

Code 2 ️⃣ Here is a set method called. It looks a little dizzy directly. It is called around. There are too many analyses to deviate. Let's talk about the key points directly.

 companion object {
        val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
     	//ComposeUiNode.SetModifier
        val SetModifier: ComposeUiNode.(Modifier) -> Unit = { this.modifier = it }
    }
// ComposeUiNode.SetModifier is also a function type. Call set(materialized, ComposeUiNode.SetModifier) 
//The execution of the SetModifier function will eventually be triggered, that is
	this.modifier=materialized //📍 modifier
 //	👆  this is the LayoutNode object by triggering composeuinode Created by constructor


I have analyzed how to trigger the SetModifier function from set(materialized, ComposeUiNode.SetModifier). This conclusion can be easily verified through debug. If you really want to analyze how to implement it, it is recommended to take a look before analyzing Thoroughly explain the implementation principle of Jetpack Compose | This article. (friendly reminder, how to really analyze this paragraph? Don't get trapped. Don't forget our purpose of looking at the source code.)

Through the above analysis, the modifier we tracked is assigned to the modifier of the LayoutNode member. This is an assignment statement, which is equivalent to the set method of the called member variable in kotlin
LayoutNode.kt

  override var modifier: Modifier = Modifier
        set(value) {
            // ...... code
            field = value
            // ...... code

        
            // Create a new LayoutNodeWrappers chain
            // foldOut is equivalent to traversing modifier
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod /*📍 modifier*/ , toWrap ->
                var wrapper = toWrap
                if (mod is OnGloballyPositionedModifier) {
                    onPositionedCallbacks += mod
                }
                if (mod is RemeasurementModifier) {
                    mod.onRemeasurementAvailable(this)
                }

                val delegate = reuseLayoutNodeWrapper(mod, toWrap)
                if (delegate != null) {
                    wrapper = delegate
                } else {
                      // ... some Modifier judgments are omitted 
                      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)
                    }
                    // Layout related modifiers
                    if (mod is LayoutModifier) {
                        wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
                    }
                    if (mod is ParentDataModifier) {
                        wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)
                    }
                   
                }
                wrapper
            }

            outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
            // Code 0 ️⃣
            outerMeasurablePlaceable.outerWrapper = outerWrapper // 👈  📍 modifier

            ......
        }

👆 Code snippet-1

The above code is mainly the process of converting the Modifier chain into the LayoutNodeWrapper chain. It traverses all elements on the Modifier chain through the foldOut function of the Modifier, and creates different layoutnodewrappers according to different modifiers. For those who don't understand the function of the Modifier's foldOut function, see what I wrote before Modifier source code, Kotlin high-order function with true 6 This article.

In the above code, different layoutnodewrappers are created according to the Modifier type. These different modifiers are all modifiers The direct implementation class or interface of element, such as KeyInputModifier, PointerInputModifier, LayoutModifier, etc. The above codes are judged by if without else, that is, if the Modifier is not within these categories, the corresponding LayoutNodeWrapper cannot be created, which means that the Modifier we set is useless. Therefore, my customized Modifier must be within the scope of this type, otherwise it is useless. Built in Modifier in JetPack Compose The subclass or interface of element is as follows. (Tips. Android studio view class inheritance menu bar navgate - > type hierarchy; shortcut key Ctrl+H)

Change your mind and continue to track

According to the above analysis, the road seems to be broken and can't continue. Think about it. This is only an analysis of the preparation for initialization when the Layout function is executed. Its size and location, if confirmed, do not seem to be performed here. We just treated ParentLayout as a parent container. A parent container generally manages the size and location of its children. In another way, ParentLayout can also be used as a child, as shown in the following code.

setContent {
    ParentLayout{
        Box() {}
        // 👇  It can be regarded as the child of the parent layout above or the parent container of the child layout below
        ParentLayout(
            Modifier
                .size(100.dp)
                .padding(10.dp)
                .background(Color.Blue)
        ) {
            ChildLayout(Modifier.size(100.dp)) {}
        }       
    }
}

Let's analyze the ParentLayout layout as a child. If it is a child, the analysis entry should start from the measure function of its parent container MeasurePolicy.

    val measurePolicy = MeasurePolicy { measurables, constraints ->
        val placeables = measurables.map { child ->
            //Code 0 ️⃣    
            child.measure(constraints)
        }
       ......
    }

Code 0 ️⃣ From the function parameters, we only know that child is a Measurable type, but Measurable is an interface. We need to know that child is the implementation class of Measurable, so that we can analyze the logic of the measure function

👆 Picture - 0

Through debug ging, we can see that the child is a LayoutNode object (we can see why it is LayoutNode in the following analysis). Then take a look at the measure function of LayoutNode.

LayoutNode.kt → measure function

 override fun measure(constraints: Constraints) =
        outerMeasurablePlaceable.measure(constraints)

The measure of LayoutNode calls the measure function of outerMeasurablePlaceable. This outerMeasurablePlaceable * * code fragment - 1 code 0 ️⃣ ** Outermeasurable placeable outerWrapper = outerWrapper // 👈 📍 Modifier, and the property outerwrapper of outerMeasurablePlaceable contains modifier information. We found modifier's hiding place again, as if we had found some clues. Let's keep tracking the code.

Analysis of measurement flow in LayoutNodeWrapper chain ⛓

OuterMeasurablePlaceable.kt

 override fun measure(constraints: Constraints): Placeable {
        ......
        remeasure(constraints)
        return this
    }
 
     fun remeasure(constraints: Constraints): Boolean {
        val owner = layoutNode.requireOwner()
         ......
        if (layoutNode.layoutState == LayoutState.NeedsRemeasure ||
            measurementConstraints != constraints
        ) {
            measuredOnce = true
            layoutNode.layoutState = LayoutState.Measuring
            measurementConstraints = constraints
            val outerWrapperPreviousMeasuredSize = outerWrapper.size
            owner.snapshotObserver.observeMeasureSnapshotReads(layoutNode) {
                outerWrapper.measure(constraints)//  0️⃣ 👈  📍 modifier
            }
            layoutNode.layoutState = LayoutState.NeedsRelayout
           ......
            return sizeChanged
        }
        return false
    }

👆 Code snippet-2
Code 0 ️⃣ At, we see that the outerWrapper containing modifier information calls its measure method. outerWrapper is of type LayoutNodeWrapper, which is the LayoutNodeWrapper chain created according to different modifs at code fragment 1. We set the modifier of ParentLayout to modifier size(100.dp). padding(10.dp). background(Color.Blue) . The corresponding LayoutNodeWrapper chain is shown in the following figure

👆 Figure-1

Figure-1 shows that the code outerWrapper at code fragment-2 is of type ModifiedLayoutNode.
ModifiedLayoutNode

internal class ModifiedLayoutNode(
    wrapped: LayoutNodeWrapper,
    modifier: LayoutModifier
) : DelegatingLayoutNodeWrapper<LayoutModifier>(wrapped, modifier) {

    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        with(modifier) {   👈  📍 modifier
            measureResult = measureScope.measure(wrapped, constraints)
            this@ModifiedLayoutNode
        }
    }
    
    protected inline fun performingMeasure(constraints: Constraints, block: () -> Placeable
    ): Placeable {
        measurementConstraints = constraints
        val result = block()
        layer?.resize(measuredSize)
        return result
    }
    
 ......   
}

It is known from figure-1 that the modifier of the code at this time is of SizeModifier type
LayoutModifier.kt

interface LayoutModifier : Modifier.Element {
	fun MeasureScope.measure(
        measurable: Measurable,/*Next LayoutNodeWrapper node*/
        constraints: Constraints/* From the parent container or from the constraint of the previous node */
    ): MeasureResult
}

SizeModifier.kt

private class SizeModifier(
    private val minWidth: Dp = Dp.Unspecified,
    private val minHeight: Dp = Dp.Unspecified,
    private val maxWidth: Dp = Dp.Unspecified,
    private val maxHeight: Dp = Dp.Unspecified,
    private val enforceIncoming: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    private val Density.targetConstraints: Constraints
        get() {/*More importantly, we generate the corresponding constraints according to the specified size*/}

    override fun MeasureScope.measure(
        measurable: Measurable,/*Next LayoutNodeWrapper*/
        constraints: Constraints/* From the parent container or from the constraint of the previous node */
    ): MeasureResult {
        val wrappedConstraints = targetConstraints.let { targetConstraints ->
            if (enforceIncoming) {//When we specify the size of the control, this value is true
            	//Combine the constraints of the parent container or the previous node with the constraints specified by us to generate a new constraint
                constraints.constrain(targetConstraints)
            } else {
                ......
            }
        }
        //Code 0 ️⃣  Make the next LayoutNodeWrapper node measurement
        val placeable = measurable.measure(wrappedConstraints)
        //After all nodes are measured, start placement
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }

Code 0 ️⃣ Continue to measure the next LayoutNodeWrapper node until the last InnerPlaceable node.

 class LayoutNode{
     internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
     private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
     ......
    internal val children: List<LayoutNode> get() = _children.asMutableList()
 }

InnerPlaceable

class InnerPlaceable{   
    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        val measureResult = with(layoutNode.measurePolicy) {
            // This is where the measure policy of the Layout is executed
            layoutNode.measureScope.measure(layoutNode.children, constraints)
        }
        layoutNode.handleMeasureResult(measureResult)
        return this
    }
}

This is where the measure of the MeasurePolicy of the Layout is executed, and then the children continue to execute the above process, as shown in the figure below, so "how is the measure method of the MeasurePolicy interface called? How does its parameter value come from?" This question is answered.

Analyze and answer questions

Through the above analysis, we can roughly answer "what is the measurement process in the layout?" That's the problem**

1. Preparation stage:

When the child parent container is declared, that is, the Layout function is called to prepare for initialization, record the measurement strategy of the child and the children of the child, and create the corresponding LayoutNodeWrapper chain according to the set Modifier chain.

2 measurement phase

Child executes child's measure function in the measure function of the parent container's measure policy. Then, execute the measure function of each node step by step according to the prepared LayoutNodeWrapper chain, and finally go to the measure function of InnerPlaceable. In this, it will continue to measure its children. At this time, its children will perform the above process like it until all children are measured.
Use the chart below to summarize the above process.

👆 Figure-2 measurement flow chart

One last question
How does setting the size through modifier work in ParentLayout?
A: we passed modifer The size() function builds a SizeModifer object

fun Modifier.size(size: Dp) = this.then(
    SizeModifier(
        minWidth = size,maxWidth = size,
        minHeight = size, maxHeight = size,
        enforceIncoming = true,...
    )
)

Through the above analysis, we know that the measure function of SizeModifer will trigger in the measurement process

SizeModifer part of the source code

private class SizeModifier(
    private val minWidth: Dp = Dp.Unspecified,
    private val minHeight: Dp = Dp.Unspecified,
    private val maxWidth: Dp = Dp.Unspecified,
    private val maxHeight: Dp = Dp.Unspecified,
    private val enforceIncoming: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    // 0 ️⃣  Generate a constraint object according to the size we set
    private val Density.targetConstraints: Constraints
    get() {
        //Controls the maxWidth value range
        val maxWidth = if (maxWidth != Dp.Unspecified) {
            maxWidth.coerceAtLeast(0.dp).roundToPx()
        } else {
            Constraints.Infinity
        }
        
        //maxHeight, minWidth and minHeight similarly handle the range of values to prevent us from making trouble
		...code...
        
        return Constraints(
            minWidth = minWidth,
            minHeight = minHeight,
            maxWidth = maxWidth,
            maxHeight = maxHeight
        )
    }

    override fun MeasureScope.measure(
        measurable: Measurable,//The name of the next node in the LayoutNodeWrapper chain
        constraints: Constraints//The parent container or constraints of the previous node in the LayoutNodeWrapper chain
    ): MeasureResult {
        val wrappedConstraints = targetConstraints.let { targetConstraints ->
                if (enforceIncoming) {
                    //one ️⃣   Continue to compare and calculate with the size we set to get a new Constraints object
                    // The constraint function is used to obtain a constraint that satisfies the size range of targetConstraints as much as possible within the constraints range
                    constraints.constrain(targetConstraints)
                } else {
					//Not for the time being
                }
                //two ️⃣   Continue with the next measurement using the new constraint
            val placeable = measurable.measure(wrappedConstraints)
            
            return layout(placeable.width, placeable.height) {
                placeable.placeRelative(0, 0)
            }
        }
    }
    
    ...
}

We passed modifier The size information set by the size function eventually becomes a member variable targetConstraints of SizeModifier. It is a constraint type, which mainly describes the minimum and maximum values of width (minWidth and maxWidth) and the minimum and maximum values of height (minHeight and maxHeight).
In the measure function, in code 1 ️⃣ Compare the passed in constraints with the size we passed in to obtain an appropriate constraint, and then use the new constraints for the next measurement. The size we set plays a role here and affects the next measurement constraint parameters.

Supplementary point - calculation process of layout width and height

Above, we went through the measure measurement process. The purpose of measurement is to calculate the width and height of the Layout. Where are these calculation processes executed? Through the Jetpack Compose write interface, each component will be converted into the corresponding node LayoutNoe. There is a process of creating LayoutNode in the Layout function. We want to analyze the final width and height of the Bureau. It depends on how the width and height of LayoutNode come from.

class LayoutNode{
    private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)

    override val width: Int get() = outerMeasurablePlaceable.width

    override val height: Int get() = outerMeasurablePlaceable.height
}

From the source code of LayoutNode, we can see that the width and height of LayoutNode is the width and height of outermeasurable placeable. We can find the width and height of outermeasurable placeable and confirm it. Above, I have roughly analyzed the measurement process, and then walk through the process in combination with the measurement flow chart in figure-2 to find the answer. Here I will not analyze it in code by code, I have drawn the width height calculation code flow of outermeasurable placeable into the following picture. If you are interested, you can look at the source code analysis in combination with the picture.

👆 Figure-3 flow chart of node width and height calculation code

The general process is to measure the child in the MeasurePolicy of the parent container - > enter the measurement function of OuterMeasurablePlaceable. Here, first calculate the width and height according to the Constraints passed in by the function, and then measure along the LayoutNodeWrapper chain. Each node of the chain calculates the width and height according to the Constraints passed in from the previous node, Until the final InnerPlaceable measurement, it also calculates the width and height according to the incoming Constraints, and then measures the children. After measuring all chlidren, it obtains a measurement result. According to this measurement result, InnerPlaceable calculates the width and height again. Then, the final width and height information is returned to the previous node. The previous node recalculates the width and height according to the returned measurement result information, and returns the measurement result in the reverse direction along the LayoutNodeWrapper chain. After recalculation, each node returns the result to the upper level until OuterMeasurablePlaceable, It gets the measurement results and recalculates the width and height. The width and height of OuterMeasurablePlaceable is the width and height of LayoutNode. The whole width and height confirmation process is roughly like this.

👆 Simple flow chart of node width and height

Each node will calculate and record the width and height according to the Constraints. At the same time, record the constraint condition, and then calculate the width and height again according to the measurement results. If you look carefully at the logic of calculating the width and height, you will find that the general reason is as follows. First, calculate the width and height according to the minimum value of the Constraints, For example, if the constraint is Constraints(minWidth = 80, maxWidth = 120, minHeight = 90, maxHeight = 150), then the width and height values are 80 and 90 respectively. After the measurement, the returned width and height result is 100120. Compare this value with the originally recorded Constraints and find that the width and height values are within the corresponding constraint range. The final width and height value shall be subject to the width and height value of the measurement result. If the measurement result is not within the constraint range, the width and height value shall be the corresponding minimum value or maximum value in the constraint conditions. The purpose of recording Constraints is to prevent measurement results from exceeding this limit.

Summary review

By tracing the source code of Layout, this paper analyzes and answers some questions. Let's review it briefly.

1. How does it work to set the size through modifier in the layout?

Answer: it is the function of calling LayoutModifier in the measurement function of ModifiedLayoutNode, such as the measure function of SizeModifer. See above for detailed analysis.

2. How is the measure method of the measurepolicy interface called?

Answer: it is called in the measure function of InnerPlaceable. See above for detailed analysis.

3. What is the measurement process in the layout and the confirmation of width and height?

Answer: see figure-2 measurement flow chart and figure-3 node width and height calculation code flow chart. See above for detailed analysis.

Topics: Android kotlin