This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

Currently, there is an ongoing Chinese manual project for Jetpack Compose, which aims to help developers better understand and master the Compose framework. It is still under construction, and everyone is welcome to follow and join! This article was written by me and has been published in the manual. Please check it out.

An overview of the

Presumably many partners in the use of Jetpack Compose development have used Modifier to modify the UI component, do Compose development partners will find the final effect of the UI component and the Modifier call sequence is closely related. This is because the Modifier will be due to the call order of different Modifier chain, Jetpack Compose in accordance with the order of the Modifier chain execution, resulting in the Modifier call order at the same time, the UI component will finally present the effect will be different. Then how to store the Modifier chain at the bottom? This article will take you together to grab the principle of the realization of the Modifier, combined with pictures to explain the underlying data structure of the Modifier chain.

Modifier interface

From the source code, we can find that the Modifier is actually an interface.

interface Modifier { 
		fun <R> foldIn(initial: R, operation: (R.Element) - >R): R
  	fun <R> foldOut(initial: R, operation: (Element.R) - >R): R
  	fun any(predicate: (Element) - >Boolean): Boolean
  	fun all(predicate: (Element) - >Boolean): Boolean
  	infix fun then(other: Modifier): Modifier = ...
  	interface Element : Modifier {. }companion object: Modifier { ... }}Copy the code

Since it is an interface, there must be a corresponding implementation. Modifier interface has three direct implementation class or interface: associated object Modifier, internal subinterface Modifier.Element, CombinedModifier.

** Companion object Modifier: ** the most commonly used Modifier, when we use the Modifier in the code. XXX (), the actual use is the companion object.

** Internal subinterface Modifier.element: ** When we use Modifier.xxx(), it actually creates an internal instance of Modifier. Let’s take size as an example. When we use Modifier. Size (100.dp), we actually create an instance of SizeModifier internally

fun Modifier.size(size: Dp) = this.then(
    SizeModifier(
        ...
    )
)
Copy the code

From the source, we can find that SizeModifier implements the LayoutModifier interface, and the LayoutModifier interface is a subinterface of the Modifier.Element.

It can be said that when we use the Modifier. XXX () created by the various types of Modifier trace to the source, and finally found that is the Modifier.Element subclass. When we use modifie.size () we create SizeModifier which is actually a subclass of the direct subinterface of the Modifier interface LayoutModifier. As shown in the figure, these interfaces basically cover all the capabilities provided by the Modifier.

CombinedModifier: Compose internal maintained data structure used to connect each Modifier node in the Modifier chain, which will be covered later.

The process of building the Modifier chain

Next, we analyze how the Modifier chain is created step by step through an example.

then()

Generally we will in the code through the associated object Modifier to create the Modifier chain. As mentioned earlier, when we use modifie.size () we create an instance of SizeModifier. When we enter the size() implementation, we find that the SizeModifier instance is passed as an argument to the then() method. And this then() method is the key to connect between the Modifier.

Modifier
    .size(100.dp)

fun Modifier.size(size: Dp) = this.then( // Key methods
    SizeModifier(
        ...
    )
)
Copy the code

At this point the this pointer is still pointing to our companion object Modifier, so let’s see how the companion object Modifier implements the then() method. As you can see, the then() method of the companion object Modifier is implemented cleanly, returning directly to the SizeModifier to be connected.

companion object : Modifier {
  	...
    override infix fun then(other: Modifier): Modifier = other
}
Copy the code

At this point the data structure of the Modifier chain is as follows

Next, we continue to call the Modifier. Background (color.red). Because this is a chain call, the current Modifier is SizeModifier, that is, when we call background, the internal use of this pointer to the SizeModifier instance.

From the source code we can see that Background actually DrawModifier implementation class, but also Modifier.Element interface implementation class

Modifier
    .size(100.dp)
		.background(Color.Red)

fun Modifier.background(
    color: Color,
    shape: Shape = RectangleShape
) = this.then( // Currently this points to the SizeModifier instance
    Background(
        ...
    )
)
Copy the code

We looked up the SizeModifier then method implementation and found it in the Modifier interface. At this point, our original SizeModifier connects to the Background through a CombinedModifier

interface Modifier {
    infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)
}

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier
Copy the code

At this point the data structure of the Modifier chain is as follows

We can see through the picture CombinedModifier through outer and inner connected to the two Modifier. However, it is worth noting that both outer and inner fields are declared with the private keyword, meaning they are not intended to be taken externally. Since the chain to chain structure storage, the official use of private keyword statement, don’t allow us to iterate the chain of Modifier. The authorities have already done that for us, through foldOut() and foldIn(), which we’ll talk about in a minute.

We continue to call the Modifier. Padding (10.dp), this pointer used inside the padding is pointing to the CombinedModifier instance, we look at the CombinedModifier then method implementation has not been rewritten, Finally returned to the interface of the Modifier.

At this point to connect is actually a PaddingModifier instance.

Modifier
    .size(100.dp)
    .background(Color.Red)
    .padding(10.dp)

fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(
            ...
        )
    )
Copy the code

At this point the data structure of the Modifier chain is as follows

composed()

Next we want to add some gesture listening, and we usually use Modifie.pointerInput () to customize gesture handling. From the source code, we can see that instead of using the then() method to connect the Modifier, we use the composed() method. From the implementation of Composed (), we can see that the then() method is still used in the end, where the ComposedModifier instance is connected. However, we know that what we really want to connect is actually the gesture processing related Modifier. From the parameter of the Composed () method, we can know that at this time, in fact, the ComposedModifier holds a factory lambda for producing the Modifier. And really want to be connected Modifier is actually lambda return values SuspendingPointerInputFilter factory. SuspendingPointerInputFilter is actually PointerInputModifier implementation class. And ComposedModifier is actually a boxing process. But when to unpack them? We’ll talk about that later.

Modifier
    .size(100.dp)
    .background(Color.Red)
    .padding(10.dp)
		.pointerInput(Unit) {... }fun Modifier.pointerInput(
    key1: Any? , block:suspend PointerInputScope. () - >Unit
): Modifier = composed( //...). {.../ / SuspendingPointerInputFilter real Modifier is gesture to deal withremember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply { ... }}fun Modifier.composed(
    inspectorInfo: InspectorInfo. () - >Unit = NoInspectorInfo,
    factory: @Composable Modifier. () - >Modifier
): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
Copy the code

At this point the data structure of the Modifier chain is as follows

By analogy, the more methods to call the Modifier chain will become longer.

Traversal of the Modifier chain

Use of foldIn() and foldOut()

Since the Modifier chain is chain structure, it can be traversed. However, as mentioned earlier, both outer and inner fields are declared with the private keyword, meaning that the outer is not available. So, the official provided us with foldOut() and foldIn() specifically for traversing the Modifier chain.

Modifier
    .size(100.dp)
    .background(Color.Red)
    .padding(10.dp)
		.pointerInput(Unit) {... }Copy the code

FoldIn () : forward traversing the Modifier chain, SizeModifier-> Background -> PaddingModifier -> ComposedModifier

FoldOut () : traverse the Modifier chain backwards, ComposedModifier -> PaddingModifier -> Background ->SizeModifier

Of course foldOut() and foldIn() need to pass arguments. There are two parameters involved: initial, operation.

fun <R> foldIn(initial: R, operation: (R.Element) - >R): R
fun <R> foldOut(initial: R, operation: (Element.R) - >R): R
Copy the code

Initial: indicates the initial value

Operation: callback each time a Modifier is traversed. This lambda takes two parameters, type R and type Element

To explain what these two parameters mean, I think the for loop analogy is a good one.

The foldIn method is similar to for (int I = initial; ; Operation ()). Setting the initial parameter is similar to setting an initial value for I, and the return value for operation will act as an update to the value.

The foldOut method is similar, but in reverse order.

That is, the return value of the operation executed when traversing the current Modifier will be passed in as the r-type parameter of the operation of the next Modifier in the chain. So may be more obscure, here is a simple example, for example, we want to count the number of Modifier in the chain of Modifier

val modifier = Modifier
    .size(100.dp)
    .background(Color.Red)
    .padding(10.dp)
    .pointerInput(Unit) {}val result = modifier.foldIn<Int> (0) { currentIndex, element ->
    Log.d("compose_study"."index: $currentIndex , element :$element")
    currentIndex + 1
}
Copy the code

FoldOut method method is similar, we are simply understood as reverse traversal Modifier chain.

Here you may have doubts, we talked about before the Modifier chain is not only the Modifier.Element, which is mixed with many CombinedModifier. Why do we iterate Modifier chain when these CombinedModifier did not appear? The reason is that CombinedModifier is actually a data structure maintained within Compose, and the official design is to hope that the upper level developers have no perception. So much for the use of these two methods, read on if you are interested in the inner workings of their implementation

How foldIn() and foldOut() are implemented

In order to explore the principle, the old rules we need to enter the source code to find out. All we need to do up here is find an implementation of the foldIn() method. Through the previous example we can know that when the length of the Modifier chain is greater than or equal to 2, return Modifier is actually a CombinedModifier instance. So let’s take a look inside the binedModifier is how to rewrite the foldIn() method.

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier {
  	...
    override fun <R> foldIn(initial: R, operation: (R.Modifier.Element) - >R): R =
        inner.foldIn(outer.foldIn(initial, operation), operation) 
}
Copy the code

You can see the first parameter passed in is outer.foldin (initi.operation) return value, through all the way up to the top of the outer Modifier. It is worth noting that the initial value we set is also passed to outer Reuters.

According to the data structure of the Modifier chain, it is easy for us to find the top outer Modifier must be a Modifier.Element, at this time we will look at the Modifier.Element is how to rewrite the foldIn() method. From the source code we see that the lambda we passed in is called directly and the return value of the lambda is returned as the return value of the foldIn() method.

interface Element : Modifier {.override fun <R> foldIn(initial: R, operation: (R.Element) - >R): R =
        operation(initial, this)}Copy the code

Next, we will retreat to the upper CombinedModifier, and then we will see how he does. This is followed by inner.foldin ()

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier {
  	...
    override fun <R> foldIn(initial: R, operation: (R.Modifier.Element) - >R): R =
        inner.foldIn(outer.foldIn(initial, operation), operation) 
}
Copy the code

Let’s take a look at the diagram for the current scenario.

The entire process is clear until the last inner Modifier is traversed and the lambda result is returned to the developer. Through the process of interpretation, we know the reason why our convenience process is not CombindedModifier, because CombinedModifier is rewriting the foldIn() method, but did not call our incoming lambda. Only all modifiers. Elements will call the lambda we passed in.

FoldIn () is the same as foldOut(), but the order of traversal is completely opposite.

Application of foldIn() and foldOut()

Now that we know how it works, let’s see how it works. The process of passing the Modifier we created into Layout in the Compose source code is a best practice for foldIn() and foldOut() methods.

We know that the Compose component is implemented based on the underlying Layout component, so let’s take a look at how the Modifier we created is passed in it. You can see that our modifier has passed in a method called materializerOf

@Composable inline fun Layout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
		...
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ...,
        update = {
						...
        },
        skippableUpdate = materializerOf(modifier), / / the key
        content = ...
    )
}
Copy the code

To follow up, we’ll go to Composer.materialize(). You can see the fouldIn() method used in the source code. In which we see the special judgment of ComposedModifier. Remember the ComposedModifier returned by Composed (). According to the previous, we know that we normally get the Modifier chain which may contain ComposedModifier, and here want to do is the Modifier chain of all ComposedModifier amortized, Let the factory inside the Modifier can also be added to the Modifier chain.

Here the fouldIn() method is used for forward traversal, and the initial value passed in is Modifier. When traversed to ComposedModifier, then use its internal Factory to produce Modifier, it is worth noting that the generated Modifier may also be the Modifier chain or a single Modifier. Generated Modifier which may also contain ComposedModifier, so here is a recursive processing. The ultimate goal is to get the Modifier chain is not included in the node ComposedModifier, that is, fully open the Modifier chain.

fun Composer.materialize(modifier: Modifier): Modifier {
		...
    val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
        acc.then(
            if (element is ComposedModifier) {
                @kotlin.Suppress("UNCHECKED_CAST")
                val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                val composedMod = factory(Modifier, this.0) Modifier / / production
                materialize(composedMod) // The generated Modifier may also contain ComposedModifier, recursive processing
            } else element
        )
    }
		...
    return result
}

Copy the code

The Modifier chain is then traversed using the foldOut method to generate the LayoutNodeWrapper chain. Understanding the nature of the Modifier chain will help you understand the process of measuring the layout in the Jetpack Compose source code. For more information, read the article “Source Analysis of the Jetpack Compose Measurement Process”.

conclusion

The purpose of this article is to lead you behind the chain of Modifier data structure and logic analysis, so that you have a clear understanding of the nature of the chain of Modifier. Figure out the nature of the chain after the use of Modifier, Modifier problems will be easy to investigate. In short, the more you know about the nature, the easier it will be to use!