Square size in Jetpack Compose

Creating a custom layout modifier to adjust size dynamically

Misha Malasai
3 min readOct 4, 2023
Photo by Vincent LaVigna on Unsplash

«In the middle of every difficulty lies opportunity.» — Albert Einstein

Problem

You are building a piece of UI that contains a button with text. The requirement for the button is to be perfectly round. It’s clear that the deciding factor here is the size of the button’s content.
How do we make a @Composable square?

Design: Row of “Amount” text and 3 following round buttons for each option: 1, 2, or 3.
Design with round buttons

Solution with fixed size

The easiest and the most obvious solution is to hardcode some good-looking size with Modifier.size(48.dp). It will not only make button’s content square, but will also ensure that all buttons have same exact size.

While this approach is very efficient, it requires manual maintenance. You’ll need to adjust specified size if the content changes, e.g. fontSize increases, e.g. user changes font size on their device.

Solution with dynamic size

The approach with fixed size has a downside of being bug-prone due to necessity of manual maintenance. To eliminate this nuance we should base the size of a square on the size of the content. If the content is measured as 100x40, we should make it a 100x100 square by extending the smallest measurement.

Attempt #1: aspectRatio() modifier

After taking a look at the List of Compose modifiers you may give aspectRatio(1f) a try. Let’s see how it goes.

Round button that takes all available space
aspectRatio() makes button’s content take all available space

Unfortunately, it goes not as one may expect. Our Text() explodes and takes all available space, though it’s square as we planned. The reason for such behaviour is implementation of aspectRatio(). It tries to match max incoming constraints, and since in our case they are limited only by device size, the text grows accordingly.

You can add sizeIn(maxWidth = 48.dp, maxHeight = 48.dp) before aspectRatio() to limit incoming constraints. However, in the essence it is just the same as solution with fixed size.

I wasn’t able to solve the problem using any out-of-the-box modifiers or their combination. Write a comment if you know how!

Attempt #2: custom layout modifier

When it is necessary to define a way of measuring and/or positioning, Compose provides LayoutModifier. That’s what we’re going to use for our implementation.

Before starting to write code, let’s decide on the algorithm.

  1. First, we limit incoming max constraints.
    If the content is measured with initial constraints straight away then in some cases we won’t be able to make the content of square size without a) cropping the content or b) getting out of constraints’ bounds.
    Max constraints are limited to the largest possible square. This way the content won’t exceed initial incoming constraints when we make it square.
  2. Next, we decide on layout size based on largest dimension of the measured content.
    By doing so we’re making sure that the other dimension doesn’t get clipped.
  3. Finally, we position the content in resulting square layout.
    In my implementation I use position: Float parameter to specify the place of the content inside the square.

Additionally, if it’s impossible to fit a square within the incoming constraints, the modifier should be ignored and the content should be laid out as if the modifier was not applied.

Visualization of the algorithm

And here’s the implementation:

Size of the square is calculated based on the size of the content

Happy coding!

--

--

Misha Malasai

Passionate Android developer and aspiring software architect