Welcome back! In lesson 15 you made text appear on the screen. Text alone makes for a plain app, though. In this lesson you will add pictures, learn how to arrange things on the screen with Row, Column and Box, and learn how to style and size them with the Modifier chain. At the end you will put all of it together in the classic beginner project: a digital business card that you could proudly show a client.
Think of arranging a shop window. You have items (a phone, a price tag, a poster). You must decide what goes on top of what, what sits beside what, how much space each one gets, and whether it has a frame around it. Compose layouts work the same way: composables are the items, Row, Column and Box are the shelves, and modifiers are the frames, padding and price stickers.
By the end of this lesson you will be able to:
- Add an image to your project and display it with
ImageandpainterResource. - Write a proper
contentDescription(and know when to usenull). - Choose between
ContentScaleoptions such asCrop,FitandFillBounds. - Arrange composables with
Row,ColumnandBox. - Control spacing and position with
ArrangementandAlignment. - Chain modifiers (
padding,size,fillMaxSize,background,clip,weight) and explain why their order matters. - Use
Iconfor small symbols from the built-in icon set. - Build and run a complete business card app from scratch.
How this fits the bigger picture: This lesson covers three items from Unit 1, Pathway 3 (Build a basic layout) of Google's Android Basics with Compose course: the Add images to your Android app codelab, the Practice: Compose Basics codelab, and the Project: Create a business card app codelab. We follow the same topics in the same order, but with our own explanations, our own code and our own card design. The pathway link is in the resources at the end.
What you need
- Android Studio set up as in lessons 02 and 03, with an emulator or a real phone.
- The ideas from lesson 15:
@Composable,Text,@PreviewandsetContent. - About 45 minutes for the worked build and another 30 for the practice and project.
- A low-end laptop is fine for this lesson. If the emulator is slow, the Compose Preview panel shows your card without running anything.
1. Composable functions (lesson 15)
A composable function is a normal Kotlin function marked with @Composable that describes a piece of UI. You call composables inside other composables, which builds a tree of UI pieces. setContent { ... } in MainActivity is where the tree starts.
2. dp and sp (lesson 15)
dp (density-independent pixels) measures sizes and spacing: 16.dp. sp (scalable pixels) measures text size: 20.sp. Both look the same physical size on different phone screens, and sp also respects the user's font-size setting.
3. Named arguments and lambdas (lessons 08 and 12)
Compose code is full of named arguments like Text(text = "Hi", fontSize = 20.sp) and trailing lambdas like Column { ... }. You should be able to read both without effort.
4. Resources in Android (a quick tour)
An Android project keeps non-code material in the res folder. You will touch two parts of it today:
| Folder | Holds | How code refers to it |
|---|---|---|
res/values/strings.xml |
text used by the app | R.string.full_name |
res/drawable |
images and vector icons | R.drawable.profile |
The generated class R is a list of IDs for everything in res. Writing R.drawable.profile means "the drawable resource named profile". The IDs are created at build time, which is why a typo is a compile error, not a crash.
Two kinds of picture
| Kind | Examples | Good for | Watch out for |
|---|---|---|---|
| Bitmap | .png, .jpg, .webp |
photographs | large files use lots of memory |
| Vector | an XML drawable | icons, logos, simple shapes | not suitable for photos |
A vector is a set of drawing instructions ("draw a curve from here to here"), so it stays sharp at any size and the file is tiny. A bitmap is a grid of coloured dots. A photo from a modern phone camera can be 12 megapixels. When Android decodes a picture it needs roughly four bytes per pixel, so such a photo needs about 48 MB of memory just to display, which is a lot on a modest phone. For a profile picture shown at 120 dp, resize the photo down first (a width of about 500 to 1000 pixels is plenty). Lesson 31 shows how libraries load big pictures from the internet efficiently.
The Image composable
Image(
painter = WHAT to draw (painterResource(R.drawable.profile))
contentDescription = WORDS for screen readers
modifier = size, shape, padding...
contentScale = HOW to fit it in the space
)
painterResource turns a drawable ID into a Painter, the object that knows how to draw the picture. The Image composable draws that painter inside whatever space the modifier gives it.
contentDescription: pictures for people who cannot see them
People who are blind or have low vision use a screen reader called TalkBack (lessons 46 to 48). When TalkBack reaches an image it reads the contentDescription aloud. So:
- Meaningful image (a photo of the person, a product photo): describe it briefly, for example
"Photo of Ade smiling". Keep descriptions instrings.xmlso they can be translated. - Decorative image (a swirl, a divider, or an icon sitting next to text that already says the same thing): pass
contentDescription = null. Then TalkBack skips it instead of reading noise. - Never leave the parameter out. It is required, which forces you to make a choice.
Rule of thumb: Ask "if this picture vanished, would a blind user miss any information?" If yes, describe it. If no, use
null.
ContentScale: fitting a picture into a space
When the picture's shape differs from the box you give it (a wide photo in a square box), you must decide what happens. ContentScale is that decision:
ContentScale |
What it does | Typical use |
|---|---|---|
Fit (the default) |
Scales the picture uniformly so all of it is visible; may leave empty space | showing a whole logo |
Crop |
Scales uniformly to fill the whole space and cuts off what overflows (centred) | profile photos, banners |
FillWidth |
Scales uniformly until the width matches | full-width banners |
FillHeight |
Scales uniformly until the height matches | tall hero images |
FillBounds |
Stretches both ways to fill the space, distorting the picture | almost never |
Inside |
Like Fit, but never scales up a small picture |
small icons |
None |
No scaling at all | pixel-perfect assets |
Wide photo in a square box
Fit Crop FillBounds
+-------+ +-------+ +-------+
| | |#######| |~~~~~~~| <- squashed
|#######| |#######| |~~~~~~~|
| | |#######| |~~~~~~~|
+-------+ +-------+ +-------+
whole photo sides cut distorted
If you remember one thing: Crop for photos that must fill a shape, Fit for things you must see completely.
Step 1: Add a drawable to the project
You have three easy routes. Menu names move between Android Studio versions, so look for the nearest match if something is not exactly where we say.
Route A: import a picture file (PNG, JPG or WebP).
- Open the Resource Manager tool window (usually a tab on the left edge, or View > Tool Windows > Resource Manager).
- Click the + button and choose Import Drawables.
- Pick your file. The file name must use only lowercase letters, digits and underscores, and must not start with a digit.
profile.pngis fine;My Photo.PNGis not. Android Studio will usually offer to rename it. - Click Next and then Import. The file lands in
res/drawable.
Route B: create a vector icon with Vector Asset.
- Right-click the
resfolder and choose New > Vector Asset. - Leave Asset Type on Clip Art, click the icon button, search for
person, and select one. - Change the Name to
profileand click Next, then Finish. This createsres/drawable/profile.xml.
Route B needs no photo and no download, which makes it ideal on a slow connection. We use it in the worked build.
Route C: drag a file into the res/drawable folder in the Project panel (in the Android view). This works too, but Route A checks the name for you.
Step 2: Show it with Image
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@Composable
fun ProfilePicture() {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = stringResource(id = R.string.profile_photo_description)
)
}
Add the matching line in res/values/strings.xml:
<string name="profile_photo_description">Profile picture of Ade</string>
What the screen shows: your picture at its natural size in the top-left corner of the screen, since we have given no modifier yet. Your project must have R imported. In a project whose package is com.example.businesscard, Android Studio normally generates the import com.example.businesscard.R automatically (or you need none if your file is in that same package). Do not accept android.R: that is a different class.
Step 3: Size it, crop it, shape it
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@Composable
fun ProfilePicture() {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = stringResource(id = R.string.profile_photo_description),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
)
}
What the screen shows: the picture cropped to fill a 120 dp square, then clipped to a circle. The order is important: size makes the space, clip cuts the shape out of it, and ContentScale.Crop fills it. You will use this recipe for nearly every avatar you ever build.
A note on Icon
For small symbols, such as a phone or an email envelope, Compose Material 3 has Icon, which draws a symbol in a single colour (the "tint"). Material's basic icon set ships with the standard Compose Material 3 dependency, so you can write:
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
@Composable
fun PhoneSymbol() {
Icon(
imageVector = Icons.Default.Phone,
contentDescription = null
)
}
Here contentDescription = null is correct when the icon sits beside text that already says the same thing (a phone icon next to a phone number). A lone icon button must have a description.
Almost every screen you will ever see is built from three layout composables. Learn these well and you can build most layouts.
Column Row Box
(top to bottom) (left to right) (stacked, back to front)
+---------+ +-----+-----+-----+ +-------------+
| one | | one | two | 3 | | back |
| two | +-----+-----+-----+ | +------+ |
| three | | |front | |
+---------+ +---+------+--+
| Layout | Places children | Think of it as |
|---|---|---|
Column |
one under another | a stack of plates, or a danfo bus aisle seen from the front |
Row |
side by side | seats along a bench |
Box |
on top of each other | sheets of paper piled up, last one on top |
The two axes: main axis and cross axis
Each layout has a direction it lays things out in (the main axis) and a direction at right angles to it (the cross axis). You control each with a different parameter. This is the part beginners mix up, so study the table:
| Main axis | Cross axis | |
|---|---|---|
| Column | vertical, controlled by verticalArrangement |
horizontal, controlled by horizontalAlignment |
| Row | horizontal, controlled by horizontalArrangement |
vertical, controlled by verticalAlignment |
| Box | (no axis) | contentAlignment places children in a 2D spot |
Arrangement decides how children are spread along the main axis. Alignment decides where they sit across the cross axis.
Arrangement options
Column(verticalArrangement = ...) on a tall space
Top (default) Center Bottom SpaceBetween SpaceEvenly
[A] . . [A] .
[B] [A] . [A]
[C] [B] . [B] .
. [C] [A] [B]
. . [B] [C] .
. . [C] [C]
.
| Arrangement | Meaning |
|---|---|
Arrangement.Top / Start |
pack at the beginning (default) |
Arrangement.Center |
pack in the middle |
Arrangement.Bottom / End |
pack at the end |
Arrangement.SpaceBetween |
first at the start, last at the end, equal gaps between |
Arrangement.SpaceAround |
equal gaps, with half-size gaps at the edges |
Arrangement.SpaceEvenly |
all gaps equal, including at the edges |
Arrangement.spacedBy(8.dp) |
a fixed gap between children, packed at the start |
Alignment options
For a Column you choose a horizontal alignment: Alignment.Start, Alignment.CenterHorizontally or Alignment.End. For a Row you choose a vertical one: Alignment.Top, Alignment.CenterVertically or Alignment.Bottom. For a Box you choose a combined position: Alignment.TopStart, Alignment.Center, Alignment.BottomEnd and so on.
Layouts only take as much space as they need
A Column with no modifier is exactly as big as its children. If you ask it to centre its children vertically, nothing will seem to happen, because there is no spare room to centre them in. That is why you will so often see Modifier.fillMaxSize() or fillMaxWidth() next to an Arrangement or Alignment: they create the spare room.
Each example below is a complete composable. Paste it into a file in your project and add a @Preview (shown once below), or drop it into setContent. All imports are listed so the code compiles as shown.
The preview you will keep reusing
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Preview(showBackground = true)
@Composable
fun LayoutPreview() {
ColumnDemo() // change this name to preview a different example
}
Example 1: A centred Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun ColumnDemo() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Techbase Academy")
Text("Ibadan, Nigeria")
Text("Learn. Build. Ship.")
}
}
What the screen shows: three lines of text stacked, exactly in the middle of the screen both ways. fillMaxSize made the Column as big as the screen; verticalArrangement = Center pushed the stack to the vertical middle; horizontalAlignment = CenterHorizontally put each line in the horizontal middle.
Example 2: A Row with an icon and text
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun EmailRow() {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(imageVector = Icons.Default.Email, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(text = "ade@example.com")
}
}
What the screen shows: a small envelope icon, a 12 dp gap, then the address, all on one line with the icon and text lined up through their vertical centres. Spacer is an invisible composable whose only job is to take up room. Without verticalAlignment, the icon would sit at the top and look slightly off.
Example 3: Weighted Row (splitting the space)
Modifier.weight hands out the leftover space in a Row or Column in proportion.
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun PriceRow() {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(text = "Jollof rice", modifier = Modifier.weight(1f))
Text(text = "₦1200", fontWeight = FontWeight.Bold)
}
}
What the screen shows: "Jollof rice" at the left edge and "₦1200" pushed to the right edge. The price takes only the width it needs, and the dish name (weight 1f) takes all the rest. If you gave two siblings weight(1f) and weight(2f), the second would get twice the room of the first.
Example 4: A Box that overlaps things
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun BadgeDemo() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color(0xFF0B3D2E))
) {
Text(
text = "NEW",
color = Color.White,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
)
Text(
text = "Fresh catch",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
What the screen shows: a dark green 200 dp square with "Fresh catch" in the centre and "NEW" in the top-right corner. Children of a Box are drawn in the order written, so a later child covers an earlier one. Modifier.align places one child; contentAlignment on the Box places all of them.
A modifier changes how a composable looks or behaves: its size, spacing, background, shape, click behaviour and more. You attach them by chaining with dots:
Modifier
.padding(16.dp)
.background(Color.Yellow)
.size(100.dp)
Each modifier wraps the result of the ones before it, like wrapping a gift: paper first, then ribbon, then a bow. Changing the order changes the gift. Compare:
Modifier.background(Yellow).padding(16.dp) Modifier.padding(16.dp).background(Yellow)
+---------------------+ (16 dp of nothing)
|YYYYYYYYYYYYYYYYYYYYY| +---------------------+
|YY +-----------+ YY| | +-------------+ |
|YY | content | YY| | |YYYYYYYYYYYYY| |
|YY +-----------+ YY| | |YY content YY| |
|YYYYYYYYYYYYYYYYYYYYY| | |YYYYYYYYYYYYY| |
+---------------------+ | +-------------+ |
padding is INSIDE the colour +---------------------+
padding is OUTSIDE the colour
(acts like a margin)
Read the chain top to bottom as outside to inside. Padding written before background creates space outside the colour (like a margin). Padding written after background creates space inside the coloured area.
The modifiers you need today
| Modifier | What it does |
|---|---|
padding(16.dp) |
space around the content (also padding(horizontal = 8.dp, vertical = 4.dp), or per-side padding(top = 8.dp)) |
size(120.dp) |
fixed width and height (also width(...), height(...)) |
fillMaxSize() |
take all the available width and height |
fillMaxWidth() / fillMaxHeight() |
take all the available width or height |
background(Color) |
a solid colour behind the content |
clip(CircleShape) |
cut the content to a shape (also RoundedCornerShape(16.dp)) |
weight(1f) |
take a share of the leftover space inside a Row or Column |
alpha(0.5f) |
make the content half transparent |
Android Studio can import each one for you: type the name, press Alt + Enter (Option + Return on a Mac) and accept the suggested import. Take care to choose the one from androidx.compose....
Pass a modifier parameter in your own composables
A good habit that you will see in all official code: give every composable you write a modifier: Modifier = Modifier parameter and pass it to the outermost layout inside. This lets whoever calls your composable decide its size and position. We do this in the build below.
Now we build a complete screen: a dark green card with a round picture, a name, a job title, and three contact lines at the bottom. The finished screen is laid out like this:
+---------------------------+
| |
| ( o ) | <- round profile picture
| |
| Ade Bello | <- name (large, bold)
| Android Developer | <- title (smaller, accent colour)
| |
| | <- this middle section grows
| [phone] +234 800 000 0000
| [share] @ade_builds |
| [email] ade@example.com |
+---------------------------+
The picture, name and title live in the top section and are centred in the room it has. The contacts sit in a bottom section. A Modifier.weight(1f) on the top section pushes the contacts to the bottom.
Step 1: Create the project
- In Android Studio choose New Project and pick Empty Activity (the Compose one; see lesson 03).
- Name it
Business Card, keep the package ascom.example.businesscard, pick a Minimum SDK that the wizard recommends, and finish. - Wait for the Gradle sync. The first sync downloads dependencies and can take several minutes on a slow connection, so be patient.
Step 2: Add the picture and the text resources
Create the vector picture with Route B from earlier: New > Vector Asset, clip art person, name it profile.
Now open res/values/strings.xml and make it look like this (keep the app_name the wizard created):
<resources>
<string name="app_name">Business Card</string>
<string name="full_name">Ade Bello</string>
<string name="job_title">Android Developer</string>
<string name="phone_number">+234 800 000 0000</string>
<string name="social_handle">@ade_builds</string>
<string name="email_address">ade@example.com</string>
<string name="profile_photo_description">Profile picture of Ade Bello</string>
</resources>
Using string resources instead of typing text inside the code makes it easy to translate the app later. The contact details above are made-up placeholders. Put your own in your own version.
Step 3: Write the card
Open MainActivity.kt and replace its contents with the following. The wizard's template changes slightly between versions (it may or may not include the enableEdgeToEdge() line or a Scaffold), so keep your own Theme import and package line if they differ. We name the theme BusinessCardTheme, which is what the wizard generates for a project called Business Card.
package com.example.businesscard
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.businesscard.ui.theme.BusinessCardTheme
private val CardBackground = Color(0xFF0B3D2E)
private val Accent = Color(0xFFFFC857)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BusinessCardTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
BusinessCard(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun BusinessCard(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(CardBackground),
horizontalAlignment = Alignment.CenterHorizontally
) {
IdentitySection(modifier = Modifier.weight(1f))
ContactSection(modifier = Modifier.padding(bottom = 32.dp))
}
}
@Composable
private fun IdentitySection(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = stringResource(id = R.string.profile_photo_description),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(Color.White)
)
Text(
text = stringResource(id = R.string.full_name),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp)
)
Text(
text = stringResource(id = R.string.job_title),
color = Accent,
fontSize = 18.sp,
modifier = Modifier.padding(top = 4.dp)
)
}
}
@Composable
private fun ContactSection(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
ContactRow(Icons.Default.Phone, stringResource(id = R.string.phone_number))
ContactRow(Icons.Default.Share, stringResource(id = R.string.social_handle))
ContactRow(Icons.Default.Email, stringResource(id = R.string.email_address))
}
}
@Composable
private fun ContactRow(icon: ImageVector, text: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Accent
)
Spacer(modifier = Modifier.width(16.dp))
Text(text = text, color = Color.White, fontSize = 16.sp)
}
}
@Preview(showBackground = true)
@Composable
fun BusinessCardPreview() {
BusinessCardTheme {
BusinessCard()
}
}
Step 4: Preview and run
- Open the Split or Design view of the editor (buttons at the top right of the editor) and click Build & Refresh if the preview is blank. The preview needs a successful build first.
- To run on a device, choose your emulator or phone in the toolbar and click the green Run button.
What the screen shows: a full-screen dark green background. In the upper part, a white circle holding the person icon, with Ade Bello in large white bold letters and Android Developer in golden yellow underneath. The three contact lines sit near the bottom, each with a golden icon on the left and white text beside it, and the three icons line up in one column.
Step 5: Read the code block by block
private val CardBackgroundandAccent: named colours at the top of the file.Color(0xFF0B3D2E)is Alpha-Red-Green-Blue in hexadecimal:FFmeans fully opaque, then0B,3D,2Efor the red, green and blue amounts. Naming colours means you change them in one place.MainActivity: wires the card into the app.Scaffoldgives usinnerPadding, the space the system bars use. We pass it into the card so the text is not hidden under the status bar.BusinessCard: an outerColumnthat fills the screen, paints it green and centres its children horizontally. It holds two children: the identity part and the contact part.IdentitySection(modifier = Modifier.weight(1f)): because ofweight(1f), this section takes all the vertical space the contacts do not need. Inside, its ownColumnusesverticalArrangement = Centerto centre the picture and names in that large space.- The picture's modifier chain:
sizefirst, thenclip, thenbackground. The white background is drawn inside the circular clip, so the transparent areas of the vector icon show white, not green. ContactRow: aRowwith an icon, aSpacerand some text, centred vertically. It is reused three times with different data, which is the main benefit of splitting the UI into small composables.contentDescription = nullon the icons: each icon sits beside text that says the same thing, so the icon is decorative.
Step 6: Experiment
Change one thing at a time and watch the preview: set verticalArrangement to Arrangement.Top; change ContentScale.Crop to ContentScale.Fit; move .background(Color.White) before .clip(CircleShape) and notice the white square corners appear; swap weight(1f) for nothing and see where the contacts go. Experiments are how layout intuition grows.
The official pathway includes a Practice: Compose Basics codelab where you apply your skills to problems with less guidance. Here is our own set in the same spirit. Try each one in the Preview before looking at the answer.
Practice 1: A greeting stack
Build a composable GreetingStack that fills the screen with a light grey background (Color(0xFFEEEEEE)). Inside, centre three Texts horizontally and vertically: "Welcome" (36 sp, bold), "to Techbase" (24 sp), and "Tap below to begin" (16 sp). Leave 12 dp between the lines using a single parameter on the Column, not three paddings.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun GreetingStack() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFEEEEEE)),
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome", fontSize = 36.sp, fontWeight = FontWeight.Bold, color = Color.Black)
Text("to Techbase", fontSize = 24.sp, color = Color.Black)
Text("Tap below to begin", fontSize = 16.sp, color = Color.Black)
}
}
The second argument, Alignment.CenterVertically, tells spacedBy to centre the whole group instead of packing it at the top. We set color = Color.Black explicitly so the text stays readable if the phone is in dark mode.
What it shows: a light grey screen with the three lines in the middle, 12 dp apart.
Practice 2: A profile row
Build ProfileRow that shows a 56 dp circular icon (use Icons.Default.Person inside an Icon with a Modifier.size(56.dp)), then 16 dp of space, then a Column holding a bold name ("Chioma Okafor") and a grey role ("Student, Ibadan"). The whole Row should have 16 dp of padding and all items should line up through the vertical centre.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ProfileRow() {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(56.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text("Chioma Okafor", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Text("Student, Ibadan", fontSize = 14.sp, color = Color.Gray)
}
}
}
What it shows: a person icon on the left, and two lines of text to its right, with the icon centred against the pair of lines. The icon has a null description because the name beside it already identifies the person.
Practice 3: A badge on a picture
Build BadgedImage: a 160 dp Box containing your profile drawable cropped to fill it, clipped to a rounded rectangle with 24 dp corners, with a small red circle (20 dp, Color.Red) overlapping the top-right corner. Use Modifier.align(Alignment.TopEnd) for the red circle.
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Composable
fun BadgedImage() {
Box(modifier = Modifier.size(160.dp)) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = "Profile picture",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(160.dp)
.clip(RoundedCornerShape(24.dp))
.background(Color.LightGray)
)
Box(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color.Red)
.align(Alignment.TopEnd)
)
}
}
What it shows: a light grey rounded square holding your picture, with a small red dot sitting in its top-right corner. The red dot comes second in the Box, so it is drawn on top. (For a real app, put that description in strings.xml. We typed it inline to keep the example short.)
Open your Business Card app and make three changes, previewing after each: (1) change the accent colour, (2) add a fourth contact line using Icons.Default.LocationOn with your town, and (3) change the picture's shape from a circle to RoundedCornerShape(24.dp). Add import androidx.compose.material.icons.filled.LocationOn for the icon.
Exercise 1: Predict the layout
Without running it, decide where the two texts appear on the screen.
Box(modifier = Modifier.fillMaxSize()) {
Text("A", modifier = Modifier.align(Alignment.BottomStart))
Text("B", modifier = Modifier.align(Alignment.BottomEnd))
}
"A" sits in the bottom-left corner of the screen and "B" sits in the bottom-right corner. The Box fills the screen, and each align picks a spot inside it. They do not overlap, because the two spots are on opposite sides.
Exercise 2: Fix the modifier order
A student wants a yellow rounded label with 16 dp of space inside the yellow, but gets a label with the text squeezed against the edge. Find the bug and fix it.
Text(
text = "Open now",
modifier = Modifier
.padding(16.dp)
.background(Color.Yellow, RoundedCornerShape(12.dp))
)
Padding written before background is outside the colour, so the yellow hugs the text and the 16 dp is an invisible margin. Swap them so the background is applied first and the padding sits inside it:
Text(
text = "Open now",
modifier = Modifier
.background(Color.Yellow, RoundedCornerShape(12.dp))
.padding(16.dp)
)
Now the yellow rounded shape is drawn around the text and the 16 dp of padding. (The background function accepts an optional shape as its second parameter. It needs import androidx.compose.foundation.background and import androidx.compose.foundation.shape.RoundedCornerShape.)
Exercise 3: A simple product card
Build a ProductCard composable inside a Column with padding(16.dp): a 200 dp-wide, 120 dp-high image area (use your profile drawable with ContentScale.Crop), then the product name "Ankara fabric" in bold, then a Row with the text "₦8500" taking the leftover width via weight(1f) and the word "In stock" at the right end.
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun ProductCard() {
Column(modifier = Modifier.padding(16.dp).width(200.dp)) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = "Ankara fabric",
contentScale = ContentScale.Crop,
modifier = Modifier.width(200.dp).height(120.dp)
)
Text(text = "Ankara fabric", fontWeight = FontWeight.Bold)
Row {
Text(text = "₦8500", modifier = Modifier.weight(1f))
Text(text = "In stock")
}
}
}
The Column is limited to 200 dp wide, so the Row inside also gets 200 dp: the price takes everything the label does not need, which pushes "In stock" to the right edge.
Mistake #1: A bad file name for an image. The wrong name: My Photo.PNG or 2024-pic.png. The problem: resource names may only contain lowercase letters, digits and underscores, and cannot start with a digit. Otherwise the build fails with an invalid resource name error. The fix: rename to my_photo.png and pic_2024.png.
Mistake #2: Importing the wrong R. The wrong code: import android.R at the top of the file, then R.drawable.profile shows red. The problem: android.R is the platform's own resource list, not your app's. The fix: delete that import, and use your project's R (it is in your package, com.example.businesscard.R, so a file in that package needs no import at all).
Mistake #3: Forgetting contentDescription. The wrong code: Image(painter = painterResource(R.drawable.profile)). The problem: the parameter is required, so the code does not compile. The fix: give a description for meaningful images, null for decorative ones.
Mistake #4: Putting a description on a decorative icon. The wrong code: an icon with contentDescription = "phone" right next to the number. The problem: TalkBack reads "phone, plus two three four..." and the learner hears it twice. The fix: use null when text beside the icon already says it.
Mistake #5: Mixing up Arrangement and Alignment. The wrong code: Column(horizontalArrangement = Arrangement.Center). The problem: a Column has no horizontalArrangement parameter; its horizontal control is horizontalAlignment. The fix: Column(horizontalAlignment = Alignment.CenterHorizontally). For a Row, it is the other way round.
Mistake #6: Centring inside a layout with no spare room. The wrong code: Column(verticalArrangement = Arrangement.Center) { ... } with no size modifier. The problem: the Column is only as tall as its children, so there is nothing to centre. The fix: add Modifier.fillMaxSize() (or a height).
Mistake #7: Modifier order surprises. The wrong code: Modifier.padding(16.dp).background(Color.Red) when you wanted the padding inside the red. The fix: background first, padding second. Read modifiers as outside to inside.
Mistake #8: Distorted pictures. The wrong code: contentScale = ContentScale.FillBounds on a photo in a mismatched box. The problem: faces get stretched. The fix: use Crop (fill the space) or Fit (show everything).
Mistake #9: Huge photos. The problem: adding a 6 MB camera photo for a 120 dp avatar slows the app and can run a modest phone out of memory. The fix: shrink the picture to a sensible size before adding it.
Mistake #10: Hard-coded text everywhere. The problem: text typed directly in code cannot be translated. The fix: use stringResource(R.string.something) for visible text.
Answer each question in your head (or on paper), then reveal the answer. These cover the topics the pathway quiz tests: images, layouts and modifiers.
Question 1. In which folder do you put an image so that you can refer to it as R.drawable.something, and what rules apply to the file name?
In res/drawable. The file name must use only lowercase letters, digits and underscores, and must not start with a digit.
Question 2. When should contentDescription be null?
When the image or icon is purely decorative, or when text right beside it already conveys the same information. Meaningful images need a short description so screen readers can speak it.
Question 3. You need a wide photo to fill a square avatar without stretching it. Which ContentScale do you pick?
ContentScale.Crop. It scales uniformly until the space is filled and cuts off the overflow. FillBounds would distort it and Fit would leave empty space.
Question 4. In a Row, which parameter spreads children along the horizontal direction, and which one aligns them vertically?
horizontalArrangement spreads them horizontally (the main axis of a Row) and verticalAlignment aligns them vertically (the cross axis).
Question 5. What is the difference between Modifier.padding(16.dp).background(Color.Red) and Modifier.background(Color.Red).padding(16.dp)?
In the first, the padding is applied first, so it lies outside the red (like a margin) and the red only covers the content. In the second, the red is drawn first and the padding is inside it, so the red area includes the 16 dp border around the content.
Go deeper: official resources
- Android Basics with Compose, Unit 1, Pathway 3: Build a basic layout: the official pathway this lesson is based on, with the three codelabs and the quiz.
- Jetpack Compose basics codelab: a longer official walkthrough of the same fundamentals.
- Jetpack Compose layouts codelab: deeper coverage of layouts and modifiers (lesson 38 builds on it).
- Customise images in Compose:
ContentScale, clipping and other image options.
Project goal: Create a second kind of card: a screen for a market stall with a name header and a small price list. The business card in the lesson stacks things in a Column; this card focuses on Row, weight and padding order, and on a card sitting on a coloured background.
Requirements:
- Create a new composable
StallCard(modifier: Modifier = Modifier). Its outermost layout is aBoxthat fills the screen, has a soft orange backgroundColor(0xFFFFF3E0)and centres its content. - Inside, a
Columnthat fills the width, has 24 dp of outer margin, a white background with rounded corners (16 dp) and 20 dp of inner padding. Mind the modifier order: margin, then clip and background, then inner padding. - At the top of the card, a
Rowwith a 48 dp orangeIcons.Default.Homeicon, a 12 dp gap, and a small Column with the stall name (bold, 22 sp) and opening hours (grey, 14 sp). All vertically centred. - A thin divider made from a
Boxthat is 1 dp high, fills the width and is light grey, with space above and below it. - Three menu rows made by one reusable private composable
MenuRow(dish: String, price: String). The dish text takes the leftover width usingweight(1f); the price is bold at the right. - Add a
@Preview. Use explicit text colours so the card also looks right in dark mode.
Stretch goal: Add a fourth row at the bottom with an Icons.Default.Phone icon (description null) and a phone number, styled like the ContactRow in the lesson.
package com.example.businesscard
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val Orange = Color(0xFFE65100)
private val TextDark = Color(0xFF212121)
@Composable
fun StallCard(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color(0xFFFFF3E0)),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(20.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
tint = Orange,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "Mama Put Kitchen",
color = TextDark,
fontSize = 22.sp,
fontWeight = FontWeight.Bold
)
Text(text = "Open 8am to 6pm", color = Color.Gray, fontSize = 14.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color.LightGray)
)
Spacer(modifier = Modifier.height(8.dp))
MenuRow(dish = "Jollof rice", price = "₦1200")
MenuRow(dish = "Fried plantain", price = "₦400")
MenuRow(dish = "Pepper soup", price = "₦1500")
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Phone,
contentDescription = null,
tint = Orange
)
Spacer(modifier = Modifier.width(12.dp))
Text(text = "+234 800 000 0001", color = TextDark, fontSize = 16.sp)
}
}
}
}
@Composable
private fun MenuRow(dish: String, price: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
) {
Text(
text = dish,
color = TextDark,
fontSize = 16.sp,
modifier = Modifier.weight(1f)
)
Text(
text = price,
color = TextDark,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
@Preview(showBackground = true)
@Composable
fun StallCardPreview() {
StallCard()
}
What the screen shows: a pale orange background with a white rounded card floating in the middle. The card has an orange house icon beside "Mama Put Kitchen" and "Open 8am to 6pm", a thin grey line, three menu lines with the dish at the left and a bold price at the right edge, and a phone line at the bottom.
Why the modifier order is right: the first padding(24.dp) comes before clip and background, so it is an outer margin that leaves the orange showing around the card. The second padding(20.dp) comes after, so it is inside the white. To display it in the app, call StallCard(modifier = Modifier.padding(innerPadding)) inside setContent in place of BusinessCard.
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 16 project”.
On github.com click New repository, name it android-lesson-16-images-layouts-and-modifiers-the-business, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 16 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: Images in Compose
- Worked Examples: Adding an Image
- Conceptual Understanding: Row, Column and Box
- Worked Examples: Layouts
- Conceptual Understanding: The Modifier Chain and Why Order Matters
- Step-by-Step Build: The Business Card App
- Practice: Compose Basics
- Guided Practice Exercises
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check