Blog Infos
Author
Published
Topics
,
Published
Exploring some usecases of BoxWithConstraints.

With the ubiquitous nature of Jetpack Compose, which enables us to create UIs on mobiles, tablets, wearables, desktops and web, it stands to reason that apps should be built with responsive layouts that look good on any screen size and orientation.

But how can we create entire screens that naturally adapt to the amount of space available? How can create building blocks that together create screens, each of which adapt to the amount of space available?

Enter BoxWithConstraints. The docs describe it as:

A composable that defines its own content according to the available space, based on the incoming constraints or the current LayoutDirection.

When I first stumbled upon it, the way I understood, that it could be used to build dynamic and responsive layouts that react to the available space. It’s quite practical for creating composables that could be used in different orientations and configurations. Let’s checkout some of it’s many usecases.

Listing items in a Card with a “+{X}” badge

 

Here “+3” is calculated based on the number of images available that were not shown.

As the name suggests, BoxWithConstraints is essentially a Box so the children will be layed out on top of each other. In this example, we have a Card child which has Column as a child of it’s own to create this layout. We will focus on the Row at the bottom of the card so let’s break down how BoxWithConstraints helps in showing thumbnails based on the space available and how the value “+3” is calculated.

@Composable
private fun Thumbnails(
thumbnails: List<String>,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier) {
val boxWithConstraintsScope = this
val padding = Theme.dimens.grid_2
val thumbnailSize = Theme.dimens.grid_6
val numberOfThumbnailsToShow = max(
0,
boxWithConstraintsScope.maxWidth.div(padding + thumbnailSize).toInt().minus(1)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(padding),
verticalAlignment = Alignment.CenterVertically,
) {
thumbnails
.take(numberOfThumbnailsToShow)
.forEach {
Image(
painter = rememberImagePainter(data = it),
contentDescription = null,
modifier = Modifier
.width(thumbnailSize)
.aspectRatio(1f),
)
}
val remaining = thumbnails.size - numberOfThumbnailsToShow
if (tagNumber > 0) {
Badge(badge = Badge.Info("+$remaining"))
}
}
}
}
view raw Thumbnails.kt hosted with ❤ by GitHub
What’s really happening here? 🤓
  • The content of BoxWithConstraints is on scope BoxWithConstraintsScope. Using that we have access to the maxWidthavailable to the layout.
  • On lines 11 to 14, numberOfThumbnailsToShow is calculated based on maxWidth and size of each thumbnail. Notice that we subtract 1 as an easy way to have space for the Badge to show +{remaining}.
  • On lines 21 to 31, we iterate through numberOfThumbnailsToShow and call the Image composable with the url and the size of image that we knew already.
  • We calculate remaining based on thumbnails.size and numberOfThumbnailsToShow and use that to display as the Badge at the end of the Row.

BoxWithConstraintsScope provides access to maxWidth, minWidth, maxHeight and minHeight, which are values in Dp. In addition to that, there is a constraints property which contains all the above 4 properties in pixels. If you are curious about the implementation of BoxWithConstraints, best place to check would be the source code.

Card in landscape mode

In landscape orientation, all the thumbnails are laid out without adding any specific cases. It just works as maxWidth property on BoxWithConstraintScope updates on changing the orientation.

One could argue that we could have used screenWidth from LocalConfiguration Composition Local and still achieve the same result, but then the composable can only be used in places where it fills the whole screen width, not really ideal. Composables should try to consider the size constraints when deciding how to render. When individual composables naturally adapt to the space available, then a screen can become reasonably responsive even if the author of the screen wasn’t explicitly thinking about responsive layout.

Let’s consider the case where we can place this card in a grid or Row on a wider screen or landscape mode. At the risk of sounding too repetitive, this just works without adding any specific cases as the constraints passed change based on the space available. Pretty neat, right? 😃

Photos from Picsum Photos

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

Displaying a defined number of items in LazyRow

In the previous example we wanted to list as many photos as possible and their size was known. In this example we want to show pre–determined number of photos and their size is based on the available space and the number of photos we would like to display.

Adjusting photo size to show 2 photos at a time

We are showing two items here, let’s jump in the code! 🤓

@Composable
private fun PhotosRow(
images: List<BeautifulCreature>,
numPhotosVisible: Float,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier = modifier) {
// Arbitrarily chosen 20 as number of "units" to divide the available width
val numGrids = 20
// Using available space to calculate the space between items and itemWidth
val spaceBetweenItems = maxWidth.div(numGrids)
val itemWidth = (maxWidth - spaceBetweenItems).div(numPhotosVisible)
LazyRow {
items(images) {
PhotoCard(
onClick = { },
photo = it.photo,
contentDescription = it.name,
modifier = Modifier
.width(itemWidth)
.aspectRatio(1f)
)
if (it != images.last()) {
Spacer(modifier = Modifier.width(spaceBetweenItems))
}
}
}
}
}
@Composable
fun PhotoCard(
onClick: () -> Unit,
photo: String,
contentDescription: String,
modifier: Modifier = Modifier,
) {
Card(
onClick = onClick,
modifier = modifier
) {
Image(
painter = rememberImagePainter(data = photo),
contentDescription = contentDescription,
contentScale = ContentScale.Crop
)
}
}
view raw PhotosRow.kt hosted with ❤ by GitHub

On lines 9 to 12, we are calculating itemWidth and spaceBetweenItems based on numPhotosVisible param passed to PhotosRow Composable. That’s it really!

Now, let’s say we want to show a bit of the third item so it feels like there are more items available to give a hint to the user that it’s scrollable. All we have to do is pass a different value for numPhotosVisible and it works like a charm!

numPhotosVisible set to 2.5f

Lazy Grid uses it under the hood

Have you ever wondered how LazyVerticalGrid is able to show dynamic number of columns based on the space available. Hmmm, Sound familiar? 🤔
Let’s peek into the Compose foundation package check how LazyVerticalGrid implements this functionality.

@ExperimentalFoundationApi
@Composable
fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
) {
val scope = LazyGridScopeImpl()
scope.apply(content)
when (cells) {
is GridCells.Fixed ->
FixedLazyGrid(
nColumns = cells.count,
modifier = modifier,
state = state,
contentPadding = contentPadding,
scope = scope
)
is GridCells.Adaptive ->
BoxWithConstraints(
modifier = modifier
) {
val nColumns = maxOf((maxWidth / cells.minSize).toInt(), 1)
FixedLazyGrid(
nColumns = nColumns,
state = state,
contentPadding = contentPadding,
scope = scope
)
}
}
}
@ExperimentalFoundationApi
sealed class GridCells {
@ExperimentalFoundationApi
class Fixed(val count: Int) : GridCells()
/**
* Combines cells with adaptive number of rows or columns. It will try to position as many rows
* or columns as possible on the condition that every cell has at least [minSize] space and
* all extra space distributed evenly.
*
* For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will be as
* many columns as possible and every column will be at least 20.dp and all the columns will
* have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
*/
@ExperimentalFoundationApi
class Adaptive(val minSize: Dp) : GridCells()
}
view raw LazyGrid.kt hosted with ❤ by GitHub
 Some comments removed to focus on BoxWithConstraints and    GridCells.Adaptive

On lines 22 to 26, we see that it actually uses BoxWithConstraints to calculate the number of cells based on the minSize passed as param of GridCells.Adaptive(minSize). Adaptive actually uses FixedLazyGridunderneath, the same as if we set cells as GridCells.Fixed. The only difference being that it calcuates the nColumns instead of the caller specifying.

Bonus

Based on the width available, we can set different discrete values of fontSizes to the Text child. There’s a code sample and example in this Jetpack Compose Playground project.

There are innumerable scenarios where BoxWithConstraints will shine. It’s always good to know about the tool and then be able to choose to use it. Feel free to comment your favourite usecases and Happy Composing! 🎼

GitHub | LinkedIn | Twitter

Thanks to Chirag Kunder, Mario Sanoguera de Lorenzo, and Jim.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu