Jetpack Compose makes it much easier to design and build your app's UI. This document explains some of the building blocks Compose provides to help you lay out your UI elements, and shows you how to build more specialized layouts when you need them.
Composable functions are the basic building block of Compose. A composable
function is a function emitting Unit
that describes some part of your UI. The
function takes some input and generates what's shown on the screen. For more
information about composables, take a look at the Compose mental
model documentation.
A Composable function might emit several UI elements. However, if you don't provide guidance on how they should be arranged, Compose might arrange the elements in a way you don't like. For example, this code generates two text elements:
@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}
Without guidance on how you want them arranged, Compose stacks the text elements on top of each other, making them unreadable:
Compose provides a collection of ready-to-use layouts to help you arrange your UI elements, and makes it easy to define your own, more-specialized layouts.
Standard layout components
In many cases, you can just use Compose's standard layout elements.
Use
Column
to place items vertically on the screen.
@Composable
fun ArtistCard() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}
Similarly, use
Row
to place items horizontally on the screen. Both Column
and Row
support
configuring the alignment of the elements they contain.
@Composable
fun ArtistCard(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image( /*...*/ )
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
Use Box
to put one element on top of another.
Often these building blocks are all you need. You can write your own composable function to combine these layouts into a more elaborate layout that suits your app.
To set children's position within a Row
, set the horizontalArrangement
and
verticalAlignment
arguments. For a Column
, set the verticalArrangement
and
horizontalAlignment
arguments:
@Composable
fun AlignInRow() {
Row(
modifier = Modifier
.size(150.dp)
.background(Color.Yellow),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.size(50.dp).background(Color.Red))
Box(Modifier.size(50.dp).background(Color.Blue))
}
}
Modifiers
Modifiers allow you to decorate or augment a composable. Modifiers let you do these sorts of things:
- Change the composable's size, layout, behavior, and appearance
- Add information, like accessibility labels
- Process user input
- Add high-level interactions, like making an element clickable, scrollable, draggable, or zoomable
Modifiers are standard Kotlin objects. Create a modifier by calling one of the
Modifier
class functions. You
can chain these functions together to compose them:
@Composable
fun ArtistCard(
artist: Artist,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.clickable(onClick = onClick)
.padding(16.dp)
.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
Spacer(Modifier.size(20.dp))
Card(elevation = 4.dp) { /*...*/ }
}
}
In the code above, notice different modifier functions used together.
clickable
makes a composable react to user input and shows a ripple.padding
puts space around an element.fillMaxWidth
makes the composable fill the maximum width given to it from its parent.size()
specifies an element's preferred width and height.
The order of modifier functions is significant. Since each function makes
changes to the Modifier
returned by the previous function, the sequence
affects the final result. Let's see an example of this:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
modifier = Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}
In the code above the whole area is clickable, including the surrounding
padding, because the padding
modifier has been applied after the clickable
modifier. If the modifiers order is reversed, the space added by padding
does
not react to user input:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}
Built-in modifiers
Jetpack Compose provides a list of built-in modifiers to help you decorate or augment a composable. Here’s a list of the ones that cover most common use cases:
Padding and size
To set padding around a composable, add the padding
modifier:
@Composable
fun PaddedComposable() {
Text("Hello World", modifier = Modifier.background(Color.Green).padding(20.dp))
}
By default, layouts provided in Compose are wrapping their children. However,
you can set a size by using the size
modifier:
@Composable
fun SizedComposable() {
Box(Modifier.size(100.dp, 100.dp).background(Color.Red))
}
Note that the size you specified might not be respected if it does not satisfy
the constraints coming from the layout's parent. If you require the composable
size to be fixed regardless of the incoming constraints, use the requiredSize
modifier:
@Composable
fun FixedSizeComposable() {
Box(Modifier.size(90.dp, 150.dp).background(Color.Green)) {
Box(Modifier.requiredSize(100.dp, 100.dp).background(Color.Red))
}
}
In this example, even with the parent width set to 90.dp
, the width of the
inner Box
will be 100.dp
, as the inner box's requiredSize
modifier takes
precedence.
If you want a child layout to fill all the available space allowed by the
parent, add the fillMaxSize
modifier (Compose also provides fillMaxHeight
and
fillMaxWidth
):
@Composable
fun FillSizeComposable() {
Box(Modifier.background(Color.Green).size(50.dp).padding(10.dp)) {
Box(Modifier.background(Color.Blue).fillMaxSize())
}
}
If you want a child layout to be the same size as a parent Box
without
affecting the Box
size, use the matchParentSize
modifier.
Note that matchParentSize
is only available within a Box
scope, meaning that
it only applies to direct children of Box
composables.
In the example below, the inner Spacer
takes its size from its parent Box
,
which in turn takes its size from theText
it contains.
@Composable
fun MatchParentSizeComposable() {
Box {
Spacer(Modifier.matchParentSize().background(Color.Green))
Text("Hello World")
}
}
If fillMaxSize
were used instead of matchParentSize
, the Spacer
would take
all the available space allowed to the parent, in turn causing the parent to
expand and fill all the available space.
If you want to add padding above a text baseline such that you achieve a
specific distance from the top of the layout to the baseline, use the
paddingFromBaseline
modifier:
@Composable
fun TextWithPaddingFromBaseline() {
Box(Modifier.background(Color.Yellow)) {
Text("Hi there!", Modifier.paddingFromBaseline(top = 32.dp))
}
}
Offset
To position a layout relative to its original position, add the offset
modifier and set the offset in the x and y axis. Offsets can be positive
as well as non-positive. The difference between padding
and offset
is that
adding an offset
to a composable does not change its measurements:
@Composable
fun OffsetComposable() {
Box(Modifier.background(Color.Yellow).size(width = 150.dp, height = 70.dp)) {
Text(
"Layout offset modifier sample",
Modifier.offset(x = 15.dp, y = 20.dp)
)
}
}
The offset
modifier is applied horizontally according to the layout direction.
In a left-to-right context, a positive offset
shifts the element to the
right, while in a right-to-left context, it shifts the element to the left.
If you need to set an offset without considering layout direction, see the
absoluteOffset
modifier, in which a positive offset value always shifts the element to the
right.
Scrollable layouts
Learn more about scrollable layouts in the Compose gestures documentation.
Responsive layouts
A layout should be designed with consideration of different screen orientations and form factor sizes. Compose offers out of the box a few mechanisms to facilitate adapting your composable layouts to various screen configurations.
Weight modifier in Row
and Column
As you have seen in the previous section on Padding and
size, a composable size is defined by the content it’s
wrapping by default. You can set a composable size to be flexible within its
parent. Let’s take a Row
that contains two two Box
composables. The first
box is given twice the weight
of the second, so it's given twice the width.
Since the Row
is 210.dp
wide, the first Box
is 140.dp
wide, and the
second is 70.dp
:
@Composable
fun FlexibleComposable() {
Row(Modifier.width(210.dp)) {
Box(Modifier.weight(2f).height(50.dp).background(Color.Blue))
Box(Modifier.weight(1f).height(50.dp).background(Color.Red))
}
}
Constraints
In order to know the constraints coming from the parent and design the layout
accordingly, you can use a BoxWithConstraints
. The measurement
constraints
can be found in the scope of the content lambda. You can use these measurement
constraints to compose different layouts for different screen configurations:
@Composable
fun WithConstraintsComposable() {
BoxWithConstraints {
Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
}
}
Slot-based layouts
Compose provides a large variety of composables based on Material
Design with the
androidx.compose.material:material
dependency (included when creating a
Compose project in Android Studio) to make UI building easy. Elements like
Drawer
,
FloatingActionButton
,
and TopAppBar
are all provided.
Material components make heavy use of slot APIs, a pattern Compose introduces
to bring in a layer of customization on top of composables. This approach makes
components more flexible, as they accept a child element which can configure
itself rather than having to expose every configuration parameter of the child.
Slots leave an empty space in the UI for the developer to fill as they wish. For
example, these are the slots that you can customize in a
TopAppBar
:
Composables usually take a content
composable lambda ( content: @Composable
() -> Unit
). Slot APIs expose multiple content
parameters for specific uses.
For example, TopAppBar
allows you to provide the content for title
,
navigationIcon
, and actions
.
For example,
Scaffold
allows you to implement a UI with the basic Material Design layout structure.
Scaffold
provides slots for the most common top-level Material components,
such as TopAppBar
,
BottomAppBar
,
FloatingActionButton
,
and Drawer
. By using
Scaffold
, it's easy to make sure these components are properly positioned and
work together correctly.
@Composable
fun HomeScreen( /*...*/ ) {
Scaffold (
drawerContent = { /*...*/ },
topBar = { /*...*/ },
bodyContent = { /*...*/ }
)
}
ConstraintLayout
ConstraintLayout
can help place composables relative to others on the screen, and is an
alternative to using multiple nested Row
, Column
, Box
and custom layouts
elements. ConstraintLayout
is useful when implementing larger layouts with
more complicated alignment requirements but when creating simple layouts,
prefer using Column
s and Row
s instead.
To use ConstraintLayout
in Compose, you need to add this dependency in your
build.gradle
:
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha03"
ConstraintLayout
in Compose works with a DSL:
- References are created using
createRefs()
orcreateRefFor()
, and each composable inConstraintLayout
needs to have a reference associated with it. - Constraints are provided using the
constrainAs()
modifier, which takes the reference as a parameter and lets you specify its constraints in the body lambda. - Constraints are specified using
linkTo()
or other helpful methods. parent
is an existing reference that can be used to specify constraints towards theConstraintLayout
composable itself.
Here's an example of a composable using a ConstraintLayout
:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
This code constrains the top of the Button
to the parent with a margin of
16.dp
and a Text
to the bottom of the Button
also with a margin of
16.dp
.
For more examples of how to work with ConstraintLayout
, try the layouts
codelab.
Decoupled API
In the ConstraintLayout
example,
constraints are specified inline, with a modifier in the composable they're
applied to. However, there are situations when it's preferable to decouple the
constraints from the layouts they apply to. For example, you might want to
change the constraints based on the screen configuration, or animate between two
constraint sets.
For cases like these, you can use ConstraintLayout
in a different way:
- Pass in a
ConstraintSet
as a parameter toConstraintLayout
. - Assign references created in the
ConstraintSet
to composables using thelayoutId
modifier.
@Composable
fun DecoupledConstraintLayout() {
WithConstraints {
val constraints = if (minWidth < 600.dp) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraints) {
Button(
onClick = { /* Do something */ },
modifier = Modifier.layoutId("button")
) {
Text("Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin = margin)
}
constrain(text) {
top.linkTo(button.bottom, margin = margin)
}
}
}
Then, when you need to change the constraints, you can just pass a different
ConstraintSet
.
Custom layouts
In Compose, UI elements are represented by the composable functions that emit a
piece of UI when invoked, that is then added to a UI tree that gets rendered on
the screen. Each UI element has one parent and potentially many children. Each
element is also located within its parent, specified as an (x, y) position, and
a size, specified as a width
and a height
.
Parents define the constraints for their child elements. An element is asked to
define its size within those constraints. Constraints restrict the minimum and
maximum width
and height
of an element. If an element has child elements, it
may measure each child to help determine its size. Once an element determines
and reports its own size, it has an opportunity to define how to place its child
elements relative to itself, as described in detail in Creating custom
layouts.
Single-pass measurement is good for performance, allowing Compose to efficiently handle deep UI trees. If an element measured its child twice and that child measured one of its children twice and so on, a single attempt to lay out a whole UI would have to do a lot of work, making it hard to keep your app performing well. However, there are times when you really need additional information on top of what a single child measurement would tell you. There are approaches that can efficiently cope with a situation like that, which are discussed in the intrinsic measurements section.
Using the layout modifier
You can use the layout
modifier to modify how an element is measured and laid
out. Layout
is a lambda; its parameters include the element you can measure,
passed as measurable
, and that composable's incoming constraints, passed as
constraints
. A custom layout modifier can look like this:
fun Modifier.customLayoutModifier(...) =
this.layout { measurable, constraints ->
...
})
Let's display a Text
on the screen and control the distance from the top to
the baseline of the first line of text. This is exactly what the
paddingFromBaseline
modifier does, we’re implementing it here as an exemple.
To do that, use the layout
modifier to manually place the composable on the
screen. Here's the desired behavior where the Text
top padding is set 24.dp
:
Here's the code to produce that spacing:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.toIntPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.place(0, placeableY)
}
}
Here's what's going on in that code:
- In the
measurable
lambda parameter, you measure theText
represented by the measurable parameter by callingmeasurable.measure(constraints)
. - You specify the size of the composable by calling the
layout(width, height)
method, which also gives a lambda used for placing the wrapped elements. In this case, it's the height between the last baseline and added top padding. - You position the wrapped elements on the screen by calling
placeable.place(x, y)
. If the wrapped elements aren't placed, they won't be visible. They
position corresponds to the top padding - the position of the first baseline of the text.
To verify this works as expected, use this modifier on a Text
:
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}
Creating custom layouts
The layout
modifier only changes the calling composable. To measure and layout
multiple composables, use the Layout
composable instead. This composable
allows you to measure and lay out children manually. All higher-level layouts
like Column
and Row
are built with the Layout
composable.
Let's build a very basic version of Column
. Most custom layouts follow this
pattern:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Similarly to the layout
modifier, measurables
is the list of children that
need to be measured and constraints
are the constraints from the parent.
Following the same logic as before, MyBasicColumn
can be implemented like
this:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints.copy(minHeight = 0))
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.place(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
The child composables are constrained by the Layout
constraints (without the
minHeight
constraints), and they're placed based on the yPosition
of the
previous composable.
Here's how that custom composable would be used:
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
MyBasicColumn(modifier.padding(8.dp)) {
Text("MyBasicColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
Layout direction
Change the layout direction of a composable by changing the
LocalLayoutDirection
compositionLocal.
If you're placing composables manually on the screen, the LayoutDirection
is
part of the LayoutScope
of the layout
modifier or Layout
composable.
When using layoutDirection
, place composables using place
. Unlike the
place
method, place
doesn't change based on the reading direction
(left-to-right versus right-to-left).
Intrinsic measurements
One of the rules of Compose is that you should only measure your children once; measuring children twice throws a runtime exception. However, there are times when you need some information about your children before measuring them.
Intrinsics lets you query children before they're actually measured.
To a composable, you can ask for its intrinsicWidth
or intrinsicHeight
:
(min|max)IntrinsicWidth
: Given this height, what's the minimum/maximum width you can paint your content properly?(min|max)IntrinsicHeight
: Given this width, what's the minimum/maximum height you can paint your content properly?
For example, if you ask the minIntrinsicHeight
of a Text
with infinite
width
, it'll return the height
of the Text
as if the text was drawn in a
single line.
Intrinsics in action
Imagine that we want to create a composable that displays two texts on the screen separated by a divider like this:
How can we do this? We can have a Row
with two Text
s inside that expands as
much as they can and a Divider
in the middle. We want the Divider to be as
tall as the tallest Text
and thin (width = 1.dp
).
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
If we preview this, we see that the Divider expands to the whole screen and that's not what we want:
This happens because Row
measures each child individually and the height of
Text
cannot be used to constraint the Divider
. We want the Divider
to fill
the available space with a given height. For that, we can use the
height(IntrinsicSize.Min)
modifier .
height(IntrinsicSize.Min)
sizes its children being forced to be as tall as
their minimum intrinsic height. As it's recursive, it'll query Row
and its
children minIntrinsicHeight
.
Applying that to our code, it'll work as expected:
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier.preferredHeight(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
With preview:
The Row
composable's minIntrinsicHeight
will be the maximum
minIntrinsicHeight
of its children. The Divider element's
minIntrinsicHeight
is 0 as it doesn't occupy space if no constraints are
given; the Text
minIntrinsicHeight
will be that of the text given a specific
width
. Therefore, the Row
element's height
constraint will be the max
minIntrinsicHeight
of the Text
s. Divider
will then expand its height
to
the height
constraint given by the Row
.
Learn more
To learn more, try the Layouts in Jetpack Compose codelab.