7

Custom Layouts, measuring policies, and BoxWithConstraints in Jetpack Compose

 2 years ago
source link: https://effectiveandroid.substack.com/p/custom-layouts-measuring-policies
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7a6644f-f399-4865-99fd-daaf3a03276d_1000x576.png

Layouts and measuring

In Jetpack Compose any Composable that emits UI is defined as a Layout, regardless of what library it belongs to (foundation, material). The Layout Composable belongs to Compose UI, which all those libraries depend on for defining their layouts. Think of Box, Column, Row… or any other Composables that emit UI.

When you write a custom Layout, you get access to a list of elements to measure (measurables), and some Constraints. Those are Constraints imposed by the parent, or a Modifier (if there are modifiers affecting the constraints that will be reflected in those Constraints):

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7164dc9-5dab-455a-b4d4-aa4961319404_1460x852.png

Inside the block, you can measure the children using the provided constraints, and then call the layout function to create the layout and place the children inside. You can read more about this in the official docs. (The trailing lambda in the Layout Composable is the MeasurePolicy used to measure the layout and its children).

The way Layouts are measured in Jetpack Compose is very similar to how Views are measured: It takes place from parent to children (from top to bottom). When a parent needs to measure its children, it imposes some Constraints so each child can be measured according to those. Note that by “parent” in this context we can also refer to a Modifier, not only a parent Layout, since modifiers can also impose additional constraints to a node (e.g: Modifier.padding).

🤲 A brief example

Let’s say we want to render a couple of Composables on screen, where each one of them takes half of the available height.

WithConstraints sample

First thought that comes to mind might be measuring screen height then setting half of it to each one of the two Composables. But that would be an ad hoc solution. What would happen if our parent layout didn’t use the whole screen height, like the following one?

WithConstraints sample

Here, the parent takes only half of the screen height. So what we need is a responsive solution that adapts to the available space imposed by the parent.

We could write this using a custom Layout in a very performant way:

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9facf589-8b5f-40f9-b2b0-a027cc2e18f1_1592x1392.png

Here we were able to place the two boxes within the custom layout, since all the information we need to create our Composable tree is already known during the Composition. When the custom layout composes it will also compose its children, and to compose the children we don’t need any extra information.

But sometimes we need Composition to actually depend on some value that is not yet available. Imagine that we want to use a different Composable depending on whether we are displaying our UI in a phone or a tablet. We would need to write some conditional logic based on the screen size. But the problem is Composition happens before that information is known. We would need to defer the composition for the children somehow. That is where BoxWithContraints comes to the rescue.

Conditional Composition

This is the actual use case ofBoxWithContraints.

BoxWithContraints is a very special Layout that doesn’t match the behaviour described above, since it does not compose its children during the composition phase. Children of BoxWithConstraints are composed during the measure/layout stage, not like the rest of the layouts in compose. This is called subcomposition across the codebase, and it is a bit of a performance overhead (that is why writing custom layouts is recommended first when possible).

The creator of a subcomposition can control when the initial composition process happens, and BoxWithConstraints decides to do it during the layout phase, as opposed to when the root is composed.

Here is an example of what I’m referring to as “conditional composition”:

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fefc78e84-bd41-44f7-b7a2-eda3e8e0609c_1360x670.png

In this example we want to make a structural change to the Composable tree based on a condition over a value that we cannot know until the layout phase.

Once we got this, it’s a good time to dig a bit into the sources 👨🏼‍💻

The sources

Let’s peek into the BoxWithConstraints sources to understand how it works.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7fff5319-b55c-4aed-8e30-e1c6bfb0ea6d_1626x1308.png

The measure policy is where the measuring logic resides. This Composable reuses the same measure policy than the Box Composable from the Compose Foundation library. This policy makes the box adapt to its wrapped children when they are not set to match the parent size. This policy can be configured in a couple ways for alignment and for propagating the min constraints from the parent. When min constraints aren’t propagated, it will then use loose the minimum constraints used to measure its children (set them to 0). That will let them decide what size they need to take.

constraints.copy(minWidth = 0, minHeight = 0)

The default behavior is actually to not propagate them.

You can learn more about measuring and layout in Compose, measure policies, and this specific one in the Jetpack Compose internals book.

But, how is the initial Composition deferred as mentioned earlier? Well, we only need to step back to the Composable definition to spot it, right at the bottom.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9291bdcd-fb7c-4cb8-9252-6140eafa3294_1834x968.png

This Composable uses SubcomposeLayout, which is an analogue of Layout that creates and runs an independent composition (subcomposition) during the layout phase. This allows child Composables to depend on any values calculated in it, like available width for example. To calculate the measurables it simply calls the subcompose function to perform subcomposition of the provided content lambda, which will include all its children, and finally proceed to measure them using the calculated measure policy and the incoming parent constraints.

👨‍🏫 Fully fledge course - “Jetpack Compose and internals”

You might want to attend the next edition of the highly exclusive “Jetpack Compose and internals” course I’m giving in October. I have carefully crafted it so attendees can master the library while learning about its internals in order to grow a correct and accurate mental mapping. This course will allow to position yourself well in the Android development industry. Limited slots left.

Enroll here


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK