Public number: byte array

Hope to help you 🤣🤣

For example, if you look at the official introduction of Jetpack Compose or the articles on the web, you might already have the impression that with Jetpack Compose you can nest the layout in a deep way without worrying about performance. Jetpack Compose is a feature that Google often uses to compare with the native View system when it comes to Jetpack Compose. In this article, we will introduce the advantages of Jetpack Compose.

  • With the native View system, we’ve always emphasized reducing the nesting level of the layout, so what’s the point of doing that
  • Jetpack Compose layout model
  • How does Jetpack Compose implement a custom layout
  • How does Jetpack Compose avoid multiple measurements
  • How does Jetpack Compose measure its inherent characteristics and how can it be adapted

Reduce the meaning of layout nesting

One of the most basic ways to optimize your application performance is to reduce the nesting level of your layout. This is something that most developers already know, but here’s why

For example, if FrameLayout nested multiple TextViews, there is only one layer of nesting

<? xml version="1.0" encoding="utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:orientation="vertical"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:padding="20dp"
        android:text="Jetpack Compose" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Yip Chi Chan" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Public number: Byte array" />

</FrameLayout>
Copy the code

The layout has the following characteristics

  • The width of FrameLayout is wrap_content, that is, the intended width is determined by the children, and the FrameLayout width is whatever the maximum width of the children is
  • The width of textView2 and textView3 is match_parent, that is, the width of the FrameLayout is the width of the FrameLayout, the width of the two TextViews

Here comes a paradox: the width of FrameLayout is determined by the maximum widths of the three children, so it is necessary to measure the maximum widths of all children before determining its own width. TextView2 and textView3 want the width to be the same as FrameLayout, so you can only determine the width of FrameLayout after you measure it. It’s like being stuck in a loop

Of course, FrameLayout has taken this situation into account when carrying out measure. The coping strategy is to carry out two measure operations, and its onMeasure method can be logically divided into two steps

As a first step, FrameLayout measures each child as normal. In this process, if FrameLayout uses wrap_content for width or height, then all children that use match_parent are saved to mMatchParentChildren

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    //layout_width or layout_height whether wrAP_content is set
    final booleanmeasureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) ! = MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) ! = MeasureSpec.EXACTLY; mMatchParentChildren.clear();int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if(mMeasureAllChildren || child.getVisibility() ! = GONE) {// Measure the width and height of the child item for the first time
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    // Save the children that use match_parentmMatchParentChildren.add(child); }}}} ···}Copy the code

In the second step, FrameLayout builds a new MeasureSpec to remeasure mMatchParentChildren, namely textView2 and textView3. The two TextViews should now have a new measuredWidth (size = measuredWidth), mode = EXACTLY, This corresponds to the value of the match_parent property

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0. getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin);  childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); }else{ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); }...// Measure the width and height of the subitem at the second timechild.measure(childWidthMeasureSpec, childHeightMeasureSpec); }}}Copy the code

Therefore, in this simple nested structure, the width between parent layout and child items is mutually affected and determined by both sides, resulting in the need to perform multiple measure operations successively to complete the layout requirements: FrameLayout once, textView1 once, textView2 and textView3 twice each, six times in total

In real development, the situation is more complicated:

  • Even if the FrameLayout is not used, the LinearLayout will have the same problem. Besides, although we don’t usually use itwrap_contentTo nestedmatch_parentBut if you use LinearLayout andlayout_weightIf so, it was measured more than once
  • If textView2 and textView3 are replaced with other viewGroups, the interlocking will cause the other children embedded in the ViewGroup to be remeasured as well
  • If the layout is used for an Activity, as ViewRootImplperformTraversals()Will be called twice when the Activity starts (API Leave 31), so FrameLayout needs to be measured twice, resulting in textView1 being measured twice, textView2 and textView3 being measured four times each, for a total of twelve times

Therefore, the deeper the nesting level of the layout structure, the number of measurements and the time taken to measure the layout will increase exponentially, resulting in a longer time for the entire view to be drawn to the screen, a lower application fluency and a worse user experience

Layout model

Jetpack Compose is a new modern Android native UI development framework, which is designed to avoid the drawbacks of the native View system. Therefore, it directly restricts multiple measurements from the bottom. A layout component cannot measure any child more than once to try different measurement configurations, or it will throw an IllegalStateException directly. This allows us to do deep nesting as needed, and the number of measurements increases linearly without worrying about performance

Use the official example for the Compose layout basics

Jetpack Compose’s layout model uses a single pass to complete the layout of the interface tree, which is implemented recursively in the same way as the original View system. First, each node is asked to measure itself, then all the child nodes are measured recursively, passing the size constraints down the tree to the child nodes. The size and placement of the leaf nodes are then determined, and the parsed size and placement instructions are passed back up the tree. In short, the parent takes measurements before its children, but adjusts itself after the size and placement of its children have been determined

For example, the following SearchResult() function generates the corresponding interface tree

@Composable
fun SearchResult(...). {
  Row(...) {
    Image(...)
    Column(...) {
      Text(...)
      Text(..)
    }
  }
}

SearchResult
  Row
    Image
    Column
      Text
      Text
Copy the code

In the SearchResult example, the interface tree layout follows the following order:

  1. The system requires a root nodeRowMeasure yourself
  2. The root nodeRowRequires that its first child node (i.eImage) to measure
  3. ImageIs a leaf node (that is, it has no children), so the node reports the size and returns a place instruction
  4. The root nodeRowRequires its second child node (i.eColumn) to measure
  5. nodeColumnRequires its first child nodeTextThe measurement
  6. Because of the first nodeTextIs a leaf node, so the node reports the size and returns a place instruction
  7. nodeColumnRequires its second child nodeTextThe measurement
  8. Because of the second nodeTextIs a leaf node, so the node reports the size and returns a place instruction
  9. Now, the nodeColumnHaving measured its children and determined their size and placement, it can now determine its own size and placement
  10. Now, the root nodeRowHaving measured its children and determined their size and placement, it can now determine its own size and placement

For example, Jetpack Compose uses Composition, Layout, and Drawing to transform state into interface elements. The Layout phase is used to determine the size and location of each node. Each node needs to complete the measurement of all the subitems before determining its own size, and the location information of the subitems can be determined only after determining its own size. If a subitem is embedded with other subitems at the same time, the subitem needs to recursively complete its measurement work according to the above steps

The measure and place order of the SearchResult() function is shown below

Custom layout

To help you understand this, implement a CustomLayout, called CustomLayout, in which each child is placed in the lower right corner in the declared order, with aligned boundaries

CustomLayout is used in exactly the same way as Row, Column and other components

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeCustomLayoutTheme {
                Surface(
                    modifier = Modifier
                        .fillMaxSize()
                        .wrapContentSize(align = Alignment.Center),
                    color = MaterialTheme.colors.background
                ) {
                    CustomLayout()
                }
            }
        }
    }

}

@Composable
private fun CustomLayout(a) {
    CustomLayout(
        modifier = Modifier
            .background(color = Color.Yellow)
    ) {
        Spacer(
            modifier = Modifier
                .background(color = Color.Green)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Cyan)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Magenta)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Red)
                .size(size = 40.dp)
        )
    }
}
Copy the code

Jetpack Compose’s custom Layout is implemented using the Layout method

@Composable 
inline fun Layout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
)
Copy the code
  • The content. All the children contained by the layout component
  • Modifier. Modifier for layout component declaration. You can use this value to declare the size of a custom layout
  • MeasurePolicy. The measurement strategy, where the dimensions and positions of all the children contained in the layout component are set

The most important part of the custom layout is to calculate the size and position of each child item. The MeasurePolicy interface provides the MeasurePolicy interface with five methods, focusing only on the Measure method

fun interface MeasurePolicy {

    fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult

}
Copy the code
  • Measurables. Each _____corresponds to a subitem in the custom layout, and is also a measure handle for that subitem, by calling its internalmeasure(constraints: Constraints)Method to measure the subterm
  • constraints. Constraints on custom layout components by their parents

To implement CustomLayout, the basic steps are:

  1. measureThe constraints parameter passed in by the Constraints method represents the layout constraints that the upper level of the CustomLayout imposes on itMinWidth, maxWidth, minHeight, maxHeight, because here do not want to let the upper level layout setminWidth 和 minHeightAffects the subitem, so reset it to 0, or build a new Constraints as needed
  2. The overall width and height of CustomLayout is obtained by summing up the width and height of all the internal sub-items, so measure the width and height of each sub-item before determining the width and height of CustomLayout itself
  3. After determining the width and height of the CustomLayout, passMeasureScope.layoutMethod to convey width and height information
  4. MeasureScope.layoutThe method provides onePlaceable.PlacementScopeScope, passes inside this scopeplaceable.place,placeable.placeRelativeAnd so on to place each of the subterms, that is, calculate the position of each of the subterms in the coordinate system here
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable() - >Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                if (measurables.isEmpty()) {
                    return layout(
                        constraints.minWidth,
                        constraints.minHeight
                    ) {}
                }
                / / the first step
                val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
                val placeables = arrayOfNulls<Placeable>(measurables.size)
                var layoutWidth = 0
                var layoutHeight = 0
                // In the second step, measure all the children and add up the width and height values of all children
                measurables.forEachIndexed { index, measurable ->
                    val placeable = measurable.measure(contentConstraints)
                    placeables[index] = placeable
                    layoutWidth += placeable.width
                    layoutHeight += placeable.height
                }
                // Step 3, pass the width and height occupied by the layout itself
                return layout(layoutWidth, layoutHeight) {
                    var top = 0
                    var left = 0
                    // Step 4, calculate the coordinate value that each child should place
                    placeables.forEach { placeable ->
                        if(placeable ! =null) {
                            placeable.place(position = IntOffset(x = left, y = top))
                            top += placeable.height
                            left += placeable.width
                        }
                    }
                }
            }
        }
    )
}
Copy the code

Intrinsic characteristic measurement

How does Jetpack Compose avoid multiple measurements

Multiple measurements are required, often because the width and height of the parent layout and child items need to be determined together, and this is a common scenario that is determined by business requirements, regardless of the UI framework we are using. In Android’s native View architecture, the initial design of the system also determines that the View structure in XML declaration will inevitably need to be measured multiple times. Jetpack Compose is a modern, new Android native UI development framework. It is natural to think about how to implement this business scenario in a more efficient way

Take the CustomLayout example. Suppose you now want to insert a Divider between the CustomLayout

Divider(
    color = Color.Black,
    modifier = Modifier
		.width(10.dp)
        .fillMaxHeight()
)
Copy the code

The Divider wants her height to match the parent’s layout, similar to the example given at the beginning of this article: The height of the CustomLayout is obtained by adding the height of all the children (except the Divider). Therefore, the Divider needs to measure all the children to determine its own height. In the measurement stage, the Divider wants to specify the height of the parent layout

If this requirement is not adapted, the Divider’s height is equal to the maximum height that the CustomLayout can get, which is the screen height, so the overall CustomLayout view ends up exceeding the entire screen

To solve this problem and avoid the need to measure multiple times, Jetpack Compose offers a solution: measurement of inherent characteristics

The official description of intrinsic characteristic measurement is not very clear, the following content is my own personal understanding, maybe a little misunderstanding, readers should have their own judgment

Or take CustomLayout as an example, first look at the use of inherent characteristics measurement, here are mainly made two changes:

  • CustomLayout throughIntrinsicSize.MinTo declare that you expect the display to be at the minimum inherent heightIntrinsicSize.Max
  • A new extension function for CustomLayout has been addedmatchParentHeight()“Is used by the Divider to indicate that the child wants to fill the parent layout height directly
@Composable
private fun CustomLayout(a) {
    CustomLayout(
        modifier = Modifier
            .height(intrinsicSize = IntrinsicSize.Min)
            .background(color = Color.Yellow)
    ) {
        Spacer(
            modifier = Modifier
                .background(color = Color.Green)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Cyan)
                .size(size = 40.dp)
        )
        Divider(
            modifier = Modifier
                .width(width = 6.dp)
                .matchParentHeight(),
            color = Color.Gray
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Magenta)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Red)
                .size(size = 40.dp)
        )
    }
}
Copy the code

Inherent characteristics measurement is used to satisfy the parent layout and the child needs to determine two wide, high This demand, it provides a mechanism for before the formal measurement to layout components to obtain in advance to the children’s expectations of the size, these expectations include: under the specific width, children can show that the minimum height and maximum height of the normal how much is respectively? And at a given height, what is the minimum and maximum width of a child item that can be displayed normally? Once you have these expected values, you know the range of sizes that the parent layout can display properly at a particular width or height

The four Intrinsic methods in MeasurePolicy represent how these expectations can be obtained. For example, when we use height(intrinsicSize = intrinsicsie.min), the layout phase calls the minIntrinsicHeight method; After using height(intrinsicSize = intrinsicsize.max), the maxIntrinsicHeight method is called

fun interface MeasurePolicy {

    fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult

    fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int

    fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int
    
}
Copy the code

Each of these four methods has a default implementation, but the default implementation may not meet our requirements, and we need to rewrite specific methods based on actual requirements. For example, we can override the minIntrinsicHeight method to calculate the minimum height CustomLayout can normally display at a given width. At this point, the minIntrinsicHeight of the other children, except for those of the matchParentHeight() method, is the minimum height CustomLayout accepts

override fun IntrinsicMeasureScope.minIntrinsicHeight(
    measurables: List<IntrinsicMeasurable>,
    width: Int
): Int {
    var maxHeight = 0
    measurables.forEach {
        if(! it.matchParentHeight()) { maxHeight += it.minIntrinsicHeight(width) } }return maxHeight
}
Copy the code

Overriding these methods affects the minWidth, maxWidth, minHeight, and maxHeight values of the Constraints values obtained by CustomLayout. Make CustomLayout aware of the minimum and maximum sizes that the caller will allow it to display

Before adapting the inherent property measurement, since CustomLayout is the root layout, the maximum size corresponding to Constraints is the screen width and height, Similar to Constraints(minWidth = 0, maxWidth = 1080, minHeight = 0, maxHeight = 1776), The Divider uses fillMaxHeight() to fill the screen height

After adapting intrinsic property measurements, because CustomLayout uses intrinsicSie.min to modify the CustomLayout semantically to display the CustomLayout at its minimum height, So the minimum and maximum heights corresponding to Constraints become the return values of the minIntrinsicHeight method. Similar to the Constraints(minWidth = 0, maxWidth = 1080, minHeight = 480, maxHeight = 480), the Divider’s height is fixed to 480 px without crossing the boundary

Therefore, the four Intrinsic methods are equivalent to a rough measurement before the formal start of the measure after the adaptation of the inherent characteristic measurement mechanism. The acceptable size range can be calculated at one time, and CustomLayout does not need to measure sub-items several times in the measure stage. Instead, we rely on Intrinsic methods to influence the measurement results of subterms, thereby avoiding multiple measurements

Adaptive inherent property measurement

How to adapt CustomLayout to inherent property measurement

In order to be able to identify which child wants to fill the layout height, you need to pass this “expectation” to CustomLayout, which means that the child needs to be able to pass parameters to the parent layout. Jetpack Compose uses the ParentDataModifier to transmit parameters. The ParentDataModifier is also used to transmit the weight parameter when using Column. IntrinsicMeasurable contains an Any? Type of the parentData parameter

CustomLayout declares its own CustomLayoutParentData class as the parameter carrier and passes the parameter values through the extension function matchParentHeight()

@LayoutScopeMarker
@Immutable
interface CustomLayoutScope {

    @Stable
    fun Modifier.matchParentHeight(a): Modifier

}

private object CustomLayoutScopeInstance : CustomLayoutScope {

    override fun Modifier.matchParentHeight(a): Modifier {
        return this.then(
            LayoutMatchParentHeightImpl(
                matchParentHeight = true,
                inspectorInfo = debugInspectorInfo {
                    name = "matchParentHeight"
                    value = true
                    properties["matchParentHeight"] = true}))}}internal data class CustomLayoutParentData(
    val matchParentHeight: Boolean = false
)

internal class LayoutMatchParentHeightImpl(
    val matchParentHeight: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {

    override fun Density.modifyParentData(parentData: Any?).: Any {
        return (parentData as? CustomLayoutParentData) ? : CustomLayoutParentData(matchParentHeight = matchParentHeight) }override fun equals(other: Any?).: Boolean {
        if (this === other) return true
        if(javaClass ! = other? .javaClass)return false
        other as LayoutMatchParentHeightImpl
        if(matchParentHeight ! = other.matchParentHeight)return false
        return true
    }

    override fun hashCode(a): Int {
        return matchParentHeight.hashCode()
    }

    override fun toString(a): String {
        return "LayoutMatchParentHeightImpl(matchParentHeight=$matchParentHeight)"}}Copy the code

Then check whether the parentData member contained in IntrinsicMeasurable is CustomLayoutParentData to get the value matchParentHeight

private fun IntrinsicMeasurable.matchParentHeight(a): Boolean {
    return (parentData as? CustomLayoutParentData)? .matchParentHeight ? :false
}
Copy the code

The final CustomLayout has the following changes:

  1. Provide an exclusive layout specific scope CustomLayoutScope for child items, ensuring thatmatchParentHeight()Methods are available only as children of CustomLayout
  2. The Divider affects the overall width of the CustomLayout but does not affect the overall height of the CustomLayout. Therefore, the Divider determines whether to add layoutHeight according to the situation during measure
  3. When calculating the coordinate value of the subitems, the other subitems remain the same except that the Y coordinate of the Divider needs to be fixed to 0
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable CustomLayoutScope. () - >Unit
) {
    Layout(
        content = { CustomLayoutScopeInstance.content() },
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {

            private fun IntrinsicMeasurable.matchParentHeight(a): Boolean {
                return (parentData as? CustomLayoutParentData)? .matchParentHeight ? :false
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                var maxHeight = 0
                measurables.forEach {
                    if(! it.matchParentHeight()) { maxHeight += it.minIntrinsicHeight(width) } }return maxHeight
            }

            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                if (measurables.isEmpty()) {
                    return layout(
                        constraints.minWidth,
                        constraints.minHeight
                    ) {}
                }
                val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
                val dividerConstraints = constraints.copy(minWidth = 0)
                val placeables = arrayOfNulls<Placeable>(measurables.size)
                val matchParentHeightChildren = mutableListOf<Placeable>()
                var layoutWidth = 0
                var layoutHeight = 0
                measurables.forEachIndexed { index, measurable ->
                    val placeable = if (measurable.matchParentHeight()) {
                        measurable.measure(dividerConstraints).apply {
                            layoutWidth += width
                            matchParentHeightChildren.add(this)}}else {
                        measurable.measure(contentConstraints).apply {
                            layoutWidth += width
                            layoutHeight += height
                        }
                    }
                    placeables[index] = placeable
                }
                return layout(layoutWidth, layoutHeight) {
                    var top = 0
                    var left = 0
                    placeables.forEach { placeable ->
                        placeable as Placeable
                        if (matchParentHeightChildren.contains(placeable)) {
                            placeable.place(position = IntOffset(x = left, y = 0))}else {
                            placeable.place(position = IntOffset(x = left, y = top))
                            top += placeable.height
                        }
                        left += placeable.width
                    }
                }
            }
        }
    )
}
Copy the code

Finally the CustomLayout will display the Divider properly

At the end

In addition to avoiding multiple measurements through the inherent feature measurement mechanism, Jetpack Compose also eliminates the need to reflect XML files into views, reducing I/O operations, which is another performance advantage of Jetpack Compose

In addition, it has greatly improved our development experience:

  • Moving from imperative to declarative allows us to focus on state management and reduces the probability of problems
  • Much less fragmentation, no need to switch back and forth between Java, Kotlin, and XML files, and Kotlin handles UI and business logic directly (although Preview is still slow at the moment).
  • Because of the differences between various Android versions, the same View architecture code often has different styles on different Android versions. As a result, we often need to define various styles and themes to ensure the unity of UI. By which to smooth out the differences between the various system versions

Jetpack Compose is currently being updated very quickly and will get better and better with continuous optimization

Finally, a complete example code is given: ComposeCustomLayout