6.
Using Compose Modifiers
Written by Denis Buketa
A beautiful UI is essential for every app. It doesn’t just look nice, it also makes your app more fun to use. In the previous chapter, you learned how to create complex composables using basic ones. You also started working on the Note and AppDrawer composables. Now, you’ll learn how to make your composables look as beautiful as they are in your ideal design.
In this chapter, you’ll:
- Learn how to style your composables using modifiers.
- Style
Noteto make it look like it should in the final design. - Add more composables to Jet Notes.
From this point on, every composable you complete will be as beautiful as in your design, by adding those modifiers you’ve been hearing about for the past few chapters. :]
Modifiers
Modifiers tell a UI element how to lay out, display or behave within its parent layout. You can also say that they decorate or add behavior to UI elements.
In the previous chapter, you started working on Note().
In the figure above, you can compare where you left off (above) with how it’ll look like by the end of this chapter (below).
To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.
Next, navigate to 06-using-compose-modifiers/projects and select the starter folder as the project root. Once the project opens, let it build and sync and you’re ready to go!
Note that if you skip ahead to the final project, you’ll be able to see the completed Note() and some other composables that you’ll implement during this chapter.
Whatever you choose, we’ll start off by building the NoteColor widget.
Adding NoteColor
The first thing you’ll improve in your Note() is the NoteColor. In the ui.components package, create a new Kotlin file named NoteColor.kt, then add the following code to it:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.Red)
)
}
@Preview
@Composable
fun NoteColorPreview() {
NoteColor()
}
To make this work, add the following imports:
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Look at the code now and you can see that you created a Box and passed a modifier to it. In this example, you used two modifier functions: Modifier.size() and Modifier.background().
Modifier.size() declares the size of the content. You pass the value in density-independent pixels (dp) and set the element width and height to the same value.
Modifier.background() draws a shape with a solid color behind the content. In this case, you passed Color.Red.
As you can see, you can easily chain several modifiers, one after the other, to combine them. In this example, you started the modifier chain with Modifier, which represents an empty modifier object.
Finally, you used NoteColorPreview() to preview your composable in the preview panel.
Build your project and check the preview and you’ll see something like this:
Congratulations, you just created a very simple composable that is 40dp in size and has a red background. Let’s see if we can make it even nicer!
Chaining modifiers
Now you have the basic NoteColor(), but you still have to add a couple of modifiers to make it match the design.
The next thing you’ll do is to make your composable’s content round. In the previous example, you saw how to chain multiple modifiers. Here, you’ll apply the same principle and change the code so it includes one additional modifier:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.Red)
.clip(CircleShape) // here
)
}
Don’t forget to include these imports as well:
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
Now, build your project and check the preview. You’ll see something like this:
Surprised? Don’t blame yourself if you tried to refresh the preview panel, expecting a different result. :]
The order of modifiers in the chain matters. Each modifier not only prepares the composable for the next modifier in the chain, but it also modifies the composable at the same time.
With this in mind, try to break down the code you wrote. With Modifier.size(), you defined the width and the height of the composable.
After that, you have Modifier.background(Color.Red). Since UI elements are represented by rectangular blocks, you end up with a red square.
Then you added Modifier.clip(), which clips the content to a specific shape. Since the two modifiers before already modified the composable, your composable didn’t change. The content remained the same.
To make this clearer, try adding another Modifier.background composable to the chain:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.Red)
.clip(CircleShape)
.background(Color.Yellow) // here
)
}
Build the project and check the preview. You’ll see something like this:
Now, you can visualize the effect that Modifier.clip() has on your composable. It clipped the future content into a circle shape, so when you applied the Modifier.background(Color.Yellow), you ended up with a yellow circle in the red square.
Taking this behavior into consideration, you can now continue working on NoteColor() to make it look like the design. Reorder the modifiers to get a circular shape with a specific color:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Red)
)
}
Here, you moved Modifier.clip() to come before the point where you specify the composable background. With that, you clipped the content of your composable to a circle whose width and height are set to the value you specify with Modifier.size().
Build the project and check the preview:
Excellent! You just created a composable that emits a colored circle of the size you specified.
Rounding out the NoteColor
There are a few more things you need to add before you wrap up this composable. One thing that’s missing is the border. To add that, update the code in NoteColor() like so:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Red)
.border( // new code
BorderStroke(
2.dp,
SolidColor(Color.Black)
),
CircleShape
)
)
}
To make Android Studio happy, add the following imports as well:
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.ui.graphics.SolidColor
Here, you added Modifier.border(), which gives you a border with the appearance you specified using the border and shape.
For the border, you passed BorderStroke(), which defined the width of the border and its color. For the shape, you used the same shape as you did when clipping the content.
Build the project and you’ll see something like this in the preview panel:
Adding some padding
If you check the design you’ll notice that there should be some padding around NoteColor(). To fix that, update the code like this:
@Composable
fun NoteColor() {
Box(
modifier = Modifier
.padding(4.dp) // here
.size(40.dp)
.clip(CircleShape)
.background(Color.Red)
.border(
BorderStroke(
2.dp,
SolidColor(Color.Black)
),
CircleShape
)
)
}
Don’t forget to add the necessary import:
import androidx.compose.foundation.layout.padding
Modifier.padding() applies additional space to each edge around the content. In the code above, you used 4.dp. Note that when you don’t specify which edge to pad, the padding will be applied to all of them. You can also specify to which edge you want to apply the padding with the following named arguments: start, top, end and bottom.
It’s important to pay attention to the order in the chain where you added the modifier. You want your circle to be the size you specify with the Modifier.size. You also want your padding to be applied around that circle. So, the best place to put Modifier.padding is just before Modifier.size.
By doing that, you’ll first apply the padding to your composable, then you’ll reserve the space of the specified size for your content.
Build the project and check the preview. It will now look like this:
Improving NoteColor’s usability
Regarding NoteColor(), you have all the necessary code to fulfill the design. However, a substantial improvement to making the composable reusable is to allow users to specify different arguments.
Right now, you’ve hard-coded the values for the size, background color, padding and border width, but your users should be able to change them.
To implement this, just expose those values as parameters by replacing NoteColor() with the following code:
@Composable
fun NoteColor(
color: Color,
size: Dp,
padding: Dp = 0.dp,
border: Dp
) {
Box(
modifier = Modifier
.padding(padding)
.size(size)
.clip(CircleShape)
.background(color)
.border(
BorderStroke(
border,
SolidColor(Color.Black)
),
CircleShape
)
)
}
Don’t forget this import:
import androidx.compose.ui.unit.Dp
Here, you changed the signature of NoteColor() to accept a color, size, padding and border. You then replaced the hard-coded values with the new parameters.
Now, you need to adapt NoteColorPreview() and specify the right parameters. Replace NoteColorPreview() with the following code:
@Preview
@Composable
fun NoteColorPreview() {
NoteColor(
color = Color.Red,
size = 40.dp,
padding = 4.dp,
border = 2.dp
)
}
Here, you’ve used the same values as before, so your preview should remain the same.
Build the project to make sure everything works as expected.
The next step is to add your new component to the Note.
Adding NoteColor to Note
Great work on completing NoteColor()! You can now use it in your Note to make it match the design.
In Note.kt, replace the Note() implementation with the following code:
@Composable
fun Note() {
Row(modifier = Modifier.fillMaxWidth()) {
NoteColor( // NoteColor instead of Box
color = rwGreen,
size = 40.dp,
padding = 4.dp,
border = 1.dp
)
Column(modifier = Modifier.weight(1f)) {
Text(text = "Title", maxLines = 1)
Text(text = "Content", maxLines = 1)
}
Checkbox(
checked = false,
onCheckedChange = { },
modifier = Modifier.padding(start = 8.dp)
)
}
}
In the code above, you removed the Box that you used as a placeholder, then added your beautiful NoteColor.
Build the project now and you’ll see something like this in the preview panel:
While you’re here, check out the modifiers that you added in the previous chapter, when you were working on Note.
For Row(), you used Modifier.fillMaxWidth(). This modifier allows you to specify the fraction of the available width that the composable should use. By default, the fraction is 1f. So in this case, you specified that the Row should take the maximum available width.
For Column(), you used Modifier.weight(). If you’re familiar with the weight property in XML layouts, then you already know what it does. With weight, you size the element’s width proportional to its weight relative to other weighted sibling elements.
Check the definition of this modifier. It’s defined in RowScope, which means you can use it on elements in a Row. In this case, you used it to make the Column take the available width between NoteColor and Checkbox.
Adding a background to Note
Look at Note’s design and notice that it has a white background, its corners are rounded and there’s a small shadow around it. Luckily, you can easily use modifiers to add those features!
Update Note() code to add the necessary modifiers to Row(), as shown below:
@Composable
fun Note() {
val backgroundShape: Shape = RoundedCornerShape(4.dp)
Row(
modifier = Modifier
.padding(8.dp)
.shadow(1.dp, backgroundShape)
.fillMaxWidth()
.preferredHeightIn(min = 64.dp)
.background(Color.White, backgroundShape)
) {
...
}
}
As usual, don’t forget to add the necessary imports:
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.foundation.layout.preferredHeightIn
With this code, you introduced four modifiers, two of which are completely new to you:
- Modifier.padding: Adds some space between the note and screen edges.
-
Modifier.shadow: Creates
DrawLayerModifier, which draws the shadow. The elevation defines the visual depth of a physical object. Furthermore, the physical object has ashape. In this case, you defined that the elevation should be1.dpand, for the shape, you usedRoundedCornerShape()to define that the corners should be rounded with a radius of4dp. - Modifier.background: This is a simple one. As you already learned, it draws a shape with a solid color behind the content.
-
Modifier.preferredHeightIn: Constrains the height of the content between
minandmaxvalues. In this case, you don’t need amaxvalue, but you do need aminbecause you want your composable to be at least64dpin height.
Now, build your project and run the app to check how Note looks.
Great! You’ve successfully added a background to your note. Pay attention to the corners: They’re rounded now, and there’s a shadow around the note. Also, see how the height of the note matches what you specified.
At this point, however, you’ve probably noticed that the overall composable looks awkward. That’s because its content isn’t centered. Fixing the alignment will be your next task.
Centering the Text & Checkbox composables
You’re getting closer and closer to completing your Note(). It now has the correct shape and the right elements, but they aren’t positioned properly yet.
Your first step is to fix the position of the Text and Checkbox. To do this, update the code for Column and Checkbox in Note() so it looks like this:
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text(text = "Title", maxLines = 1)
Text(text = "Content", maxLines = 1)
}
Checkbox(
checked = false,
onCheckedChange = { },
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically)
)
For this to work, add this import:
import androidx.compose.ui.Alignment
The key to aligning your composables in this code is Modifier.align(alignment: Alignment.Vertical). This modifier allows you to align elements vertically within the Row.
Just like Modifier.weight(), this modifier is defined in a RowScope. That means you can only use it in a Row.
Notice that you also added Modifier.padding() to the checkbox to make it look nicer. This doesn’t affect the composable alignment, but it’s a good practice to pay attention to the details.
Build and run the app and your note will look like this:
Now, both the text and the checkbox are nicely centered in the note.
Centering NoteColor
When you look at NoteColor, you realize that you can’t apply a modifier to it like you did for the Column and Checkbox. The NoteColor doesn’t expose a modifier as its parameter. It’s time to fix that!
In NoteColor.kt, update NoteColor() so it looks like this:
@Composable
fun NoteColor(
modifier: Modifier = Modifier, // 1
color: Color,
size: Dp,
padding: Dp = 0.dp,
border: Dp
) {
Box(
modifier = modifier // 2
.padding(padding)
.size(size)
.clip(CircleShape)
.background(color)
.border(
BorderStroke(
border,
SolidColor(Color.Black)
),
CircleShape
)
)
}
There are two things to notice here:
- You added a modifier as a parameter to your custom composable and you initialized it with an empty
Modifier. - You used that modifier as the first in your chain of modifiers in
Box(). Remember that, earlier, you used an emptyModifierhere instead.
Note: Take the time to digest the difference between using
modifiervsModifier. Only one single character separates the two, but the meaning is completely different. Understanding this can help you avoid quite a few bugs in the future.
What you just did is considered a good practice when creating custom composables with Jetpack Compose. It’s always useful to expose the modifier as a parameter and to allow users of that composable to add other modifiers, as needed.
Now, go back to Note.kt and align the NoteColor as well:
NoteColor(
modifier = Modifier.align(Alignment.CenterVertically),
color = rwGreen,
size = 40.dp,
padding = 4.dp,
border = 1.dp
)
As you see, you can now apply the same logic as you did for the Text and Checkbox, so you add Modifier.align() to NoteColor.
Build and run the app.
Nice! Every component is now nicely centered in the note. However, the NoteColor and the Text are a bit cramped on the left side of the note. You’ll work on that next.
Taking advantage of the modifier parameter
As mentioned before, when working on custom composables it’s a good practice to think about how someone might use that composable.
For NoteColor you exposed the color, size, padding, border and modifier to make it more flexible. However, you have to be careful not to overdo it. Having a lot of parameters can introduce more complexities than you need.
By exposing the modifier as a parameter, you suddenly allow a lot of customization for your composable. That means that you might be able to remove some parameters because the behavior they provided can now be taken over by the modifier.
For NoteColor, notice that you’re passing the padding as a parameter. That was useful when you didn’t have the modifier as a parameter, but now, you don’t need it. You’ll do something about that next. :]
Open NoteColor.kt and update the code to look like this:
@Composable
fun NoteColor(
modifier: Modifier = Modifier,
color: Color,
size: Dp,
border: Dp
) {
Box(
modifier = modifier
.size(size)
.clip(CircleShape)
.background(color)
.border(
BorderStroke(
border,
SolidColor(Color.Black)
),
CircleShape
)
)
}
@Preview
@Composable
fun NoteColorPreview() {
NoteColor(
color = Color.Red,
size = 40.dp,
border = 2.dp
)
}
In the code above, you removed padding from the composable’s parameters. You also removed Modifier.padding() from the chain of modifiers for the Box. You also updated NoteColorPreview so it doesn’t include padding in the parameters.
Now, you’ll get the padding by calling the composable through the modifier.
Applying the padding
In Note.kt, update NoteColor like this:
NoteColor(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 16.dp, end = 16.dp), // here
color = rwGreen,
size = 40.dp,
border = 1.dp
)
To add horizontal padding to NoteColor, you added Modifier.padding() to the Modifier that you pass as one of its parameters.
Build and run the app, and you’ll see the following result:
Great job, your note almost matches the design now!
However, the devil is in the details, and there’s one thing still missing: the text style. Right now, both the title and the content have the same text style. You’ll work on this next.
Styling title and content
Look at the note design once again and you’ll see that the title and content have specific text styles. The content text is smaller and has a different color. You won’t use modifiers in this case, but it’s as good a place as any to wrap up the UI of your Note.
In Note.kt, edit the code for Column() so it looks like this:
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text(
text = "Title",
color = Color.Black,
maxLines = 1,
style = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
letterSpacing = 0.15.sp
)
)
Text(
text = "Content",
color = Color.Black.copy(alpha = 0.75f),
maxLines = 1,
style = TextStyle( // here
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
letterSpacing = 0.25.sp
)
)
}
To avoid complaints from Android Studio, add these imports:
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
Here, you used the style and color in your Texts to apply Material Design to your note.
TextStyle() is a styling configuration for the Text. It exposes different parameters like fontWeight, fontSize, letterSpacing and more that let you style the text.
In Chapter 8, “Applying Material Design To Compose”, you’ll see how you can use Material components provided in Jetpack Compose to easily accomplish the same result. But for now, it’s good to notice that you can accomplish the same thing using basic components.
Build and run the app. The note now looks like this:
Well done! Your note composable is now as beautiful as it is in the design. :]
Now that you’ve completed the Note, it would be nice to add some new composables to Jet Notes.
Adding the Color composable
No, you’re not experiencing deja vu. This will be a different composable from the previous NoteColor. :]
Since you’ve completed NoteColor, it makes sense to add a composable that relies on it to build extra functionality.
So now, you’ll start working on a color picker composable, like the one shown below:
The color picker allows the user to color code their notes by assigning specific colors to them. The user can open the color picker by clicking on the color palette icon in the app bar or by pulling from the bottom edge of the screen.
You can break this composable down into smaller ones, as shown in the figure above. By following the bottom-up approach, you’ll work on ColorItem first.
You’ll use this composable in the Save Note screen.
Creating the ColorItem
Start by creating a new package called screens. Then, in this package, create a new Kotlin file named SaveNoteScreen.kt. Finally, add the following code to SaveNoteScreen.kt:
@Composable
fun ColorItem(
color: ColorModel,
onColorSelect: (ColorModel) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = {
onColorSelect.invoke(color)
}
)
) {
NoteColor(
modifier = Modifier.padding(10.dp),
color = Color.fromHex(color.hex),
size = 80.dp,
border = 2.dp
)
Text(
text = color.name,
fontSize = 22.sp,
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.CenterVertically)
)
}
}
For this to work, you need to add the necessary imports:
import androidx.compose.material.Text
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.raywenderlich.android.jetnotes.domain.model.ColorModel
import com.raywenderlich.android.jetnotes.ui.components.NoteColor
import androidx.compose.ui.graphics.Color
import com.raywenderlich.android.jetnotes.util.fromHex
OK, it’s time to break down the code. In the design, two components work together to make the ColorItem composable: NoteColor and Text. They’re aligned next to each other, so you use a Row to position them.
There’s one new modifier here that you haven’t used so far: Modifier.clickable() in Row. With that modifier, you made the whole ColorItem clickable. As mentioned before, it’s a good practice to expose click events to parent composables.
To accomplish that, you passed the onColorSelect.invoke(color) call for the onClick. onColorSelect is of type (ColorModel) -> Unit, which is known as a function type. This specific function type says that the function that will be passed to it should take ColorModel as an argument.
To execute it, you used the invoke(color) operator. This means that when the user clicks on a note, the function that was passed for the onColorSelect parameter will execute.
ColorItem has two parameters:
- A
colorparameter of typeColorModel, which represents a model class for the color. - The
onColorSelectparameter of type(ColorModel) -> Unit. This is a lambda that takesColorModelas an argument. That way, you allow the parent composable to know which color the user selected.
Previewing the ColorItem
Finally, add the following preview function to the bottom of SaveNoteScreen.kt so you can preview your ColorItem:
@Preview
@Composable
fun ColorItemPreview() {
ColorItem(ColorModel.DEFAULT) {}
}
Don’t forget to add the Preview import:
import androidx.compose.ui.tooling.preview.Preview
Here, you just invoked Color with the default color defined in ColorModel.kt. For onColorSelect, you passed an empty lambda since you don’t need it for the preview to work. Thanks to Kotlin, you’re able to pass the second argument as a trailing lambda.
Now, build the project. In the preview panel, you’ll see this:
Great work! You’ve completed another composable! :]
Now, you can use this composable to complete the color picker.
Wrapping up the ColorPicker composable
With ColorItem in place, it’s a piece of cake to build ColorPicker().
Add the following code to the top of SaveNoteScreen.kt, just above ColorItem:
@Composable
private fun ColorPicker(
colors: List<ColorModel>,
onColorSelect: (ColorModel) -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Color picker",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(8.dp)
)
ScrollableColumn(modifier = Modifier.fillMaxWidth()) {
for (color in colors) {
ColorItem(color, onColorSelect)
}
}
}
}
As usual, there are a few imports that you need to add as well:
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.text.font.FontWeight
To create the ColorPicker in the code above, you used a Column to align its title and list of colors. You want the user to be able to scroll through the colors, so you used a ScrollableColumn to wrap them.
ColorPicker has two parameters: It takes the list of ColorModels and, like the ColorItem, it exposes the click event parameter.
To visualize what you’ve built so far, add the preview composable to the bottom of SaveNoteScreen.kt:
@Preview
@Composable
fun ColorPickerPreview() {
ColorPicker(
colors = listOf(
ColorModel.DEFAULT,
ColorModel.DEFAULT,
ColorModel.DEFAULT
)
) { }
}
Here, you invoked ColorPicker() and passed it a list of default colors. For onColorSelect, you passed an empty lambda, since you’re not interested in interacting with the composable at this stage.
Build the project and check the preview panel to see this:
Well done! Yet another composable under your belt. :]
You’ll see the color picker in action in Chapter 7, “Managing State in Compose”.
This is where this chapter ends. Hopefully, you now have a feeling for how powerful modifiers are. You can find the final code for this chapter by navigating to 06-using-compose-modifiers/projects/final.
Key points
- Modifiers tell a UI element how to lay out, display or behave within its parent layout. You can also say that they decorate or add behavior to UI elements.
- You can chain several modifiers, one after the other, to compose them.
- The order of modifiers in the chain matters. Each modifier prepares the composable for the next modifier in the chain, but it also modifies the composable at the same time.
- Avoid hard-coding the values in your composables. Instead, expose those values as properties of the composable function.
- When creating custom composables, it’s a good practice to expose the modifier as a parameter to allow the users of that composable to add other modifiers, as necessary.
Where to go from here?
Modifiers are a great tool to use when you style your composables. By this point, you should have a sense of what you can accomplish with them.
This chapter didn’t cover all the modifiers that Compose offers since there are a lot of them. The good news is that the principles are the same so you should feel safe using them with the knowledge you’ve gained.
When you play with composables, don’t be afraid to dive deep and research which modifiers you can use on which components. You might be pleasantly surprised. :]
In the next chapter, you’ll learn one of the most important things about Jetpack Compose: how to manage states. When you complete that chapter, Jet Notes will be one step closer to being a fully functional app.