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 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 gravity of the elements they contain.
@Composable
fun ArtistCard(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image( /*...*/ )
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
Use Stack
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.
Each of these basic layouts defines its own gravity settings, specifying how the elements should be arranged. To configure these elements, use modifiers.
Modifiers
Modifiers allow you to tweak how a composable is presented. Modifiers let you do these sorts of things:
- Change the composable's 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
) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
Spacer(Modifier.preferredSize(padding))
Card(elevation = 4.dp) { /*...*/ }
}
}
In the code above, notice different modifier functions used together.
clickable()
makes a composable react to user input.padding()
puts space around an element.fillMaxWidth()
makes the composable fill the maximum width given to it from its parent.preferredSize()
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
.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 are applied
in the other order, 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
}
}
Scrollable layouts
Use
ScrollableRow
or
ScrollableColumn
to make the elements inside a Row
or Column
scroll.
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
ScrollableColumn(Modifier.fillMaxSize()) {
feedItems.forEach {
ArtistCard(it, onSelected)
}
}
}
This approach works well if there are few elements to show, but can quickly
become a performance concern for large data sets. To show only part of the
elements visible on the screen, use
LazyColumnFor
or
LazyRowFor
.
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
Surface(Modifier.fillMaxSize()) {
LazyColumnFor(feedItems) { item ->
ArtistCard(item, onSelected)
}
}
}
Built-in Material components
Compose's highest level of UI abstraction is Material
Design. Compose provides a large variety of
composables out of the box 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. 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
.
The most high-level composable with Material is
Scaffold
.
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 Row
, Column
, and Stack
elements.
ConstraintLayout
is useful when implementing larger layouts with more
complicated alignment requirements.
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 thetag
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)
}
}
}
Then, when you need to change the constraints, you can just pass a different
ConstraintSet
.
Custom layouts
Some composable functions 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 also has a location within its parent,
specified as an (x, y) position, and a size, specified as a width
and a
height
.
Elements are asked to define their own Constraints that should be satisfied.
Constraints restrict the minimum and maximum width
and height
of an element.
If an element has child elements, the parent may measure each of the children to
help determine the parent's size. Once an element reports its own size, it has an
opportunity 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 a layout 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 Using the layout modifier.
Using the layout modifier
You can use the layout
modifier to modify how a composable is measured and
laid out. Layout
is a lambda; its parameters include the composable you can
measure, passed as measurable
, and that composable's passed constraints,
passed as constraints
. Most custom layout
modifiers follow this pattern:
fun Modifier.customLayoutModifier(...) =
Modifier.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. 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.placeRelative(0, placeableY)
}
}
Here's what's going on in that code:
- In the
measurable
lambda parameter, you measure theText
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 children. In this case, it's the height between the last baseline and added top padding. - You position the children on the screen by calling
placeable.placeRelative(x, y)
. If the children aren't placed, they won't be visible. They
position corresponds to the top padding - the position of the first baseline of the text.placeRelative
automatically mirrors the position in right-to-left contexts.
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 one composable. To manually control 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 simple implementation of Column
. Most custom layouts follow this
pattern:
@Composable
fun MyOwnColumn(
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 passed to the
Layout
. Following the same logic as before, MyOwnColumn
can be implemented
like this:
@Composable
fun MyOwnColumn(
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)
}
// 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.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
The child composables are constrained by the Layout
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) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
Layout direction
Change the layout direction of a composable by using the LayoutDirection
ambient.
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
placeRelative
method, place
doesn't change based on the reading direction
(left-to-right versus right-to-left).
Learn more
To learn more, try the Layouts in Jetpack Compose codelab.