28

Android Jetpack Compose contains width(), height() and size() layout modifiers as well as requiredWidth(), requiredHeight() and requiredSize(). What is the difference between these two sets of modifiers? Should I use plain modifiers or required ones?

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123

2 Answers2

50

The difference is that plain modifiers like width() take into account layout Constraints while required modifiers like requiredWidth() ignore them. You can think of constrains as min/max width/height of a measured element. To better understand how layout modifiers and constraints work take a look at this post.

Let's see an example of width modifier, height and size behave the same way.

@Composable
fun WidthModifierExample() {
    val columnWidth = 200.dp

    Column(
        modifier = Modifier
            .width(columnWidth)
            .border(1.dp, Color.Gray)
    ) {
        Text(
            text = "requiredWidth = parent - 50",
            modifier = Modifier
                .requiredWidth(columnWidth - 50.dp)
                .background(Color.LightGray)
        )
        Text(
            text = "requiredWidth = parent + 50",
            modifier = Modifier
                .requiredWidth(columnWidth + 50.dp)
                .background(Color.LightGray)
        )
        Text(
            text = "width = parent - 50",
            modifier = Modifier
                .width(columnWidth - 50.dp)
                .background(Color.LightGray),
        )
        Text(
            text = "width = parent + 50",
            modifier = Modifier
                .width(columnWidth + 50.dp)
                .background(Color.LightGray)
        )
    }
}

enter image description here

The dark gray border is the column boundaries, the texts background is light gray. Modifier.width(200.dp) is applied to the column so it receives minWidth = 200dp; maxWidth = 200dp constraints. The Column layout doesn't limit its children minimum width so each child gets minWidth = 0; maxWidth = 200dp constraints. The first two texts use requiredWidth() modifier which ignores this constraints so the second text is wider than the parent column. The last text width is reduced to 200dp according to the maxWidth = 200dp constraint because it uses width() modifier instead.

In the example above only the maximum width of the texts was constrained. Lets see another example where the minimum width is constrained as well:

@Composable
fun WidthModifierExample() {
    Column(
        modifier = Modifier.border(1.dp, Color.Gray)
    ) {
        val minWidth = 140
        val maxWidth = 200
        val widthDescriptions = arrayOf(
            WidthDescription(minWidth - 50, "min - 50"),
            WidthDescription((minWidth + maxWidth) / 2, "between min and max"),
            WidthDescription(maxWidth + 50, "max + 50")
        )

        for (widthDescription in widthDescriptions) {
            Text(
                text = "requiredWidth = ${widthDescription.description}",
                modifier = Modifier
                    .border(.5.dp, Color.Red)
                    .widthIn(minWidth.dp, maxWidth.dp)
                    .background(Color.LightGray)
                    .requiredWidth(widthDescription.width.dp)
            )
        }

        for (widthDescription in widthDescriptions) {
            Text(
                text = "width = ${widthDescription.description}",
                modifier = Modifier
                    .border(.5.dp, Color.Red)
                    .widthIn(minWidth.dp, maxWidth.dp)
                    .background(Color.LightGray)
                    .width(widthDescription.width.dp)
            )
        }
    }
}

data class WidthDescription(
    val width: Int,
    val description: String
)

enter image description here

In this case the Column() doesn't add any constraints but each child Text() is wrapped in widthIn() modifier which adds its own constrains. The light gray background shows the texts size while the red border shows dimensions of the resulting element. As you can see requiredWidth() modifier of the first three texts ignores the outer constrains, as the result the element width and its child Text() width might be different. The last three texts uses width() modifier which takes into account the constrains so Text() width is always matches the resulting element.

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
13

In jetpack Compose dimensions of Composable or child Composables are set using Constraints which is set of dimensions and flags for being bounded or finite. Modifier.requiredX are used for forcing size but can easily break your layout if the constraints are not in limit of parent's.

Difference between widthIn and requiredWidthIn is first one abides size modifiers before it or max parent dimensions while latter can force constraints.

/**
 * Create a [Constraints]. [minWidth] and [minHeight] must be positive and
 * [maxWidth] and [maxHeight] must be greater than or equal to [minWidth] and [minHeight],
 * respectively, or [Infinity][Constraints.Infinity].
 */
@Stable
fun Constraints(
    minWidth: Int = 0,
    maxWidth: Int = Constraints.Infinity,
    minHeight: Int = 0,
    maxHeight: Int = Constraints.Infinity
): Constraints {
    require(maxWidth >= minWidth) {
        "maxWidth($maxWidth) must be >= than minWidth($minWidth)"
    }
    require(maxHeight >= minHeight) {
        "maxHeight($maxHeight) must be >= than minHeight($minHeight)"
    }
    require(minWidth >= 0 && minHeight >= 0) {
        "minWidth($minWidth) and minHeight($minHeight) must be >= 0"
    }
    return Constraints.createConstraints(minWidth, maxWidth, minHeight, maxHeight)
}

based on Constraints during measurement width and height are assigned to Composables as Placeables.

Size Modifiers

Modifier.width/height()

Returns fixed Constraints or one with equal minWidth/Height and maxWidth/Height. So Child Composables are limited to this Constraints

Modifier.fillmaxWidth/Height(fraction)

Covers fraction of space that is available from parent's max constraints

Modifier.width/heightIn(min)

Reserves minimum width/height as this value so child Composables can be measured using this dimension. Children can be lower than this value or greater until max width/height

Modifier.width/heightIn(max)

This is the max width/height children can be measured with. Children Composables cannot be measured with dimension bigger than max width/height.

@Composable
private fun ConstraintsSample1() {
    Text(text = "Fixed Size")
    BoxWithConstraints(modifier = Modifier
        .size(100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Red))
    }

    Spacer(modifier=Modifier.height(10.dp))
    BoxWithConstraints(modifier = Modifier
        .size(100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(150.dp)
            .background(Color.Red))
    }

    Text(text = "widthIn(min)")

    BoxWithConstraints(modifier = Modifier
        .widthIn(min = 100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Red))
    }

    Spacer(modifier=Modifier.height(10.dp))
    BoxWithConstraints(modifier = Modifier
        .widthIn(min = 100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(150.dp)
            .background(Color.Red))
    }


    Text(text = "widthIn(max)")

    BoxWithConstraints(modifier = Modifier
        .widthIn(max = 100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Red))
    }

    Spacer(modifier=Modifier.height(10.dp))
    BoxWithConstraints(modifier = Modifier
        .widthIn(max = 100.dp)
        .border(3.dp, Color.Green)) {
        Box(modifier = Modifier
            .size(150.dp)
            .background(Color.Red))
    }
}

Modifier.requiredWidth/Height()

Constrain the width/height of the content to be between `minDp` and `maxDp`. If the content chooses a size that does not satisfy the incoming Constraints, the parent layout will be reported a size coerced in the Constraints, and the position of the content will be automatically offset to be centered on the space assigned to the child by the parent layout under the assumption that Constraints were respected.

For instance when you set Modifier.size(50.dp).size(100.dp) first size modifier is applied however if you user requiredIn

Modifier.size(50.dp).requiredSizeIn(100.dp) 

100.dp is forced but layout is placed as half of difference between 2 dimensions

@Composable
private fun ConstraintsSample2() {

    Column(modifier = Modifier
        .fillMaxSize()
        .padding(30.dp)
        .border(4.dp, Color.Cyan)) {

        Text(text = "Chaining size modifiers")

        Box(modifier = Modifier
            .size(50.dp)
            .background(Color.Yellow))

        Box(modifier = Modifier
            .size(50.dp)
            .size(100.dp)
            .background(Color.Red))

        Box(modifier = Modifier
            .size(50.dp)
            .requiredSizeIn(100.dp)
            .background(Color.Green))


        Text(text = "widthIn(max)")

        BoxWithConstraints(
            modifier = Modifier
                .width(100.dp)
                .border(3.dp, Color.Green)
        ) {
            Box(
                modifier = Modifier
                    .requiredWidthIn(min = 20.dp, max = 50.dp)
                    .height(50.dp)
                    .background(Color.Red)
            )
        }

        Spacer(modifier = Modifier.height(10.dp))
        BoxWithConstraints(
            modifier = Modifier
                .width(100.dp)
                .border(3.dp, Color.Green)
        ) {
            Box(
                modifier = Modifier
                    .requiredWidthIn(min = 150.dp, max = 200.dp)
                    .height(50.dp)
                    .background(Color.Red)
            )
        }
    }
}

In the sample below when TextField doesn't have parent with fixed size it grows till max width. If fixed size that is smaller than TextField is set when Modifier.widthIn() is used TextField is set to parent width while Modifier.requiredWidthIn() jumps out of parent as (TextField size - parent size)/2

enter image description here

@Composable
private fun TextFieldSamples() {

    Column(
        modifier = Modifier
            .padding(20.dp).border(2.dp, Color.Cyan)
    ) {

        var text1 by remember { mutableStateOf("") }

        Column(modifier = Modifier) {
            TextField(
                modifier = Modifier
                    .border(2.dp, Color.Green)
                    .widthIn(56.dp),
                value = text1,
                onValueChange = { text1 = it }
            )

            TextField(
                modifier = Modifier
                    .border(2.dp, Color.Red)
                    .requiredWidthIn(56.dp),
                value = text1, onValueChange = { text1 = it })
        }

        Spacer(modifier = Modifier.height(30.dp))

        var text2 by remember { mutableStateOf("") }

        Column(modifier = Modifier.width(50.dp)) {
            TextField(
                modifier = Modifier
                    .border(2.dp, Color.Green)
                    .widthIn(56.dp),
                value = text2, onValueChange = { text2 = it })

            TextField(
                modifier = Modifier
                    .border(2.dp, Color.Red)
                    .requiredWidthIn(56.dp),
                value = text2, onValueChange = { text2 = it })
        }
    }
}

Constraints

Constraints change based on size modifiers, vertical/horizontal scroll or how parent chooses to limit or want to use min or max dimension.

Max device on my device width is 1080px, and 200.dp is 525px in the samples below. And used verticalScroll that's why height is measured with Constraints.Infinity

@Composable
fun ConstraintsSample3() {
    Column(modifier = Modifier) {

        Text(text = "No Dimension Modifier")

        BoxWithConstraints(modifier = Modifier.background(Brown400)) {
            val hasBoundedWidth = constraints.hasBoundedWidth
            val hasFixedWidth = constraints.hasFixedWidth
            val minWidth = constraints.minWidth
            val maxWidth = constraints.maxWidth

            val hasBoundedHeight = constraints.hasBoundedHeight
            val hasFixedHeight = constraints.hasFixedHeight
            val minHeight = constraints.minHeight
            val maxHeight = constraints.maxHeight
            Text(
                "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                        "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                        "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                        "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
                color = Color.White
            )
        }

        Spacer(modifier = Modifier.height(10.dp))
        Text(text = "FillMaxWidth and 200.dp Height")
        BoxWithConstraints(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Red400)
        ) {
            val hasBoundedWidth = constraints.hasBoundedWidth
            val hasFixedWidth = constraints.hasFixedWidth
            val minWidth = constraints.minWidth
            val maxWidth = constraints.maxWidth

            val hasBoundedHeight = constraints.hasBoundedHeight
            val hasFixedHeight = constraints.hasFixedHeight
            val minHeight = constraints.minHeight
            val maxHeight = constraints.maxHeight
            Text(
                "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                        "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                        "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                        "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
                color = Color.White
            )
        }

        Spacer(modifier = Modifier.height(10.dp))
        Text(text = "wrapContentSize()")
        BoxWithConstraints(
            modifier = Modifier
                .wrapContentSize()
                .background(Orange400)
        ) {

            val hasBoundedWidth = constraints.hasBoundedWidth
            val hasFixedWidth = constraints.hasFixedWidth
            val minWidth = constraints.minWidth
            val maxWidth = constraints.maxWidth

            val hasBoundedHeight = constraints.hasBoundedHeight
            val hasFixedHeight = constraints.hasFixedHeight
            val minHeight = constraints.minHeight
            val maxHeight = constraints.maxHeight
            Text(
                "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                        "hasBoundedWidth: $hasBoundedWidth\n" +
                        "hasFixedWidth: $hasFixedWidth\n" +
                        "minHeight: $minHeight\n" +
                        "maxHeight: $maxHeight\n" +
                        "hasBoundedHeight: $hasBoundedHeight\n" +
                        "hasFixedHeight: $hasFixedHeight",
                color = Color.White
            )
        }

        Spacer(modifier = Modifier.height(10.dp))
        Text(text = "200.dp Width and Height")
        BoxWithConstraints(
            modifier = Modifier
                .width(200.dp)
                .height(200.dp)
                .background(Green400)
        ) {

            val hasBoundedWidth = constraints.hasBoundedWidth
            val hasFixedWidth = constraints.hasFixedWidth
            val minWidth = constraints.minWidth
            val maxWidth = constraints.maxWidth

            val hasBoundedHeight = constraints.hasBoundedHeight
            val hasFixedHeight = constraints.hasFixedHeight
            val minHeight = constraints.minHeight
            val maxHeight = constraints.maxHeight
            Text(
                "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                        "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                        "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                        "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
                color = Color.White
            )
        }
    }
}
@Composable
private fun BoxWithConstraintsSample4() {
    Text(text = "200.dp WidthIn(min) and HeightIn(min)")
    BoxWithConstraints(
        modifier = Modifier
            .widthIn(min = 200.dp)
            .heightIn(200.dp)
            .background(Blue400)
    ) {

        val hasBoundedWidth = constraints.hasBoundedWidth
        val hasFixedWidth = constraints.hasFixedWidth
        val minWidth = constraints.minWidth
        val maxWidth = constraints.maxWidth

        val hasBoundedHeight = constraints.hasBoundedHeight
        val hasFixedHeight = constraints.hasFixedHeight
        val minHeight = constraints.minHeight
        val maxHeight = constraints.maxHeight
        Text(
            "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                    "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                    "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                    "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
            color = Color.White
        )
    }

    Text(text = "200.dp requiredWidth(min) and requiredHeight(min)")
    BoxWithConstraints(
        modifier = Modifier
            .requiredWidthIn(min = 200.dp)
            .requiredHeightIn(200.dp)
            .background(Pink400)
    ) {

        val hasBoundedWidth = constraints.hasBoundedWidth
        val hasFixedWidth = constraints.hasFixedWidth
        val minWidth = constraints.minWidth
        val maxWidth = constraints.maxWidth

        val hasBoundedHeight = constraints.hasBoundedHeight
        val hasFixedHeight = constraints.hasFixedHeight
        val minHeight = constraints.minHeight
        val maxHeight = constraints.maxHeight
        Text(
            "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                    "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                    "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                    "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
            color = Color.White
        )
    }

    Text(text = "200.dp defaultMinSize()")
    BoxWithConstraints(
        modifier = Modifier
            .defaultMinSize(200.dp)
            .background(Pink400)
    ) {

        val hasBoundedWidth = constraints.hasBoundedWidth
        val hasFixedWidth = constraints.hasFixedWidth
        val minWidth = constraints.minWidth
        val maxWidth = constraints.maxWidth

        val hasBoundedHeight = constraints.hasBoundedHeight
        val hasFixedHeight = constraints.hasFixedHeight
        val minHeight = constraints.minHeight
        val maxHeight = constraints.maxHeight
        Text(
            "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                    "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                    "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                    "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
            color = Color.White
        )
    }

    Text(text = "200.dp WidthIn(max)")
    BoxWithConstraints(
        modifier = Modifier
            .widthIn(max = 200.dp)
            .background(Purple400)
    ) {

        val hasBoundedWidth = constraints.hasBoundedWidth
        val hasFixedWidth = constraints.hasFixedWidth
        val minWidth = constraints.minWidth
        val maxWidth = constraints.maxWidth

        val hasBoundedHeight = constraints.hasBoundedHeight
        val hasFixedHeight = constraints.hasFixedHeight
        val minHeight = constraints.minHeight
        val maxHeight = constraints.maxHeight
        Text(
            "minWidth: $minWidth, maxWidth: $maxWidth\n" +
                    "hasBoundedWidth: $hasBoundedWidth, hasFixedWidth: $hasFixedWidth\n" +
                    "minHeight: $minHeight, maxHeight: $maxHeight\n" +
                    "hasBoundedHeight: $hasBoundedHeight, hasFixedHeight: $hasFixedHeight",
            color = Color.White
        )
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • What is `Placeables` ? – Kotlin Learner Dec 07 '22 at 11:46
  • Placeable is what you get from `measureable.measure(constraints)`. You can position a Compoasble using `placeable.place/placeRelative/placeWithLayer` inside parent. Check this [answer's](https://stackoverflow.com/questions/70904979/how-align-to-bottom-a-row-in-jetpack-compose/70905004#70905004) custom layout part. And there are many custom layout samples in this [tutorial](https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials#3-2-1-custom-layout) with different constraints. To create a custom layout you measure using constraints get placeable then place it according to requirements – Thracian Dec 07 '22 at 12:00
  • Perfect thanks for great explain. I am learning all of these because of this [issue](https://stackoverflow.com/q/74701726/11560810). Do you have any idea, how to solve this? – Kotlin Learner Dec 07 '22 at 12:02
  • @vivekmodi i explained here how to use layout, constraints, measurable, placeable and layoutId. It might help. https://stackoverflow.com/questions/74730550/how-to-use-xmls-androidid-in-jetpack-compose/74731534#74731534 I will create a mega answer about how to use these and create custom layouts – Thracian Dec 08 '22 at 15:39
  • One more question related to compose, which coverage tool are you using ? – Kotlin Learner Dec 08 '22 at 18:23