Techbase Academy
Android · Lesson 16

Images, Layouts and Modifiers: The Business Card App

12 phasesMarket Stall CardAndroid Studio
Phase 1 of 12
Lesson Introduction

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 Image and painterResource.
  • Write a proper contentDescription (and know when to use null).
  • Choose between ContentScale options such as Crop, Fit and FillBounds.
  • Arrange composables with Row, Column and Box.
  • Control spacing and position with Arrangement and Alignment.
  • Chain modifiers (padding, size, fillMaxSize, background, clip, weight) and explain why their order matters.
  • Use Icon for 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, @Preview and setContent.
  • 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.
✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 12
Prerequisite Concepts

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.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 12
Conceptual Understanding: Images in Compose

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

text
  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 in strings.xml so 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
text
  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.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Images in Compose. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 12
Worked Examples: Adding an Image

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).

  1. Open the Resource Manager tool window (usually a tab on the left edge, or View > Tool Windows > Resource Manager).
  2. Click the + button and choose Import Drawables.
  3. Pick your file. The file name must use only lowercase letters, digits and underscores, and must not start with a digit. profile.png is fine; My Photo.PNG is not. Android Studio will usually offer to rename it.
  4. Click Next and then Import. The file lands in res/drawable.

Route B: create a vector icon with Vector Asset.

  1. Right-click the res folder and choose New > Vector Asset.
  2. Leave Asset Type on Clip Art, click the icon button, search for person, and select one.
  3. Change the Name to profile and click Next, then Finish. This creates res/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

kotlin
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:

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

kotlin
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:

kotlin
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.

✏️ Your Task
Practise what you just learned about Worked Examples: Adding an Image. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 12
Conceptual Understanding: Row, Column and Box

Almost every screen you will ever see is built from three layout composables. Learn these well and you can build most layouts.

text
  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

text
  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.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Row, Column and Box. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 12
Worked Examples: Layouts

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

kotlin
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

kotlin
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

kotlin
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.

kotlin
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

kotlin
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.

✏️ Your Task
Practise what you just learned about Worked Examples: Layouts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 12
Conceptual Understanding: The Modifier Chain and Why Order Matters

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:

kotlin
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:

text
  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.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: The Modifier Chain and Why Order Matters. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 12
Step-by-Step Build: The Business Card App

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:

text
  +---------------------------+
  |                           |
  |         (  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

  1. In Android Studio choose New Project and pick Empty Activity (the Compose one; see lesson 03).
  2. Name it Business Card, keep the package as com.example.businesscard, pick a Minimum SDK that the wizard recommends, and finish.
  3. 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):

xml
<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.

kotlin
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

  1. 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.
  2. 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 CardBackground and Accent: named colours at the top of the file. Color(0xFF0B3D2E) is Alpha-Red-Green-Blue in hexadecimal: FF means fully opaque, then 0B, 3D, 2E for the red, green and blue amounts. Naming colours means you change them in one place.
  • MainActivity: wires the card into the app. Scaffold gives us innerPadding, the space the system bars use. We pass it into the card so the text is not hidden under the status bar.
  • BusinessCard: an outer Column that 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 of weight(1f), this section takes all the vertical space the contacts do not need. Inside, its own Column uses verticalArrangement = Center to centre the picture and names in that large space.
  • The picture's modifier chain: size first, then clip, then background. The white background is drawn inside the circular clip, so the transparent areas of the vector icon show white, not green.
  • ContactRow: a Row with an icon, a Spacer and 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 = null on 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.

✏️ Your Task
Practise what you just learned about Step-by-Step Build: The Business Card App. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 12
Practice: Compose Basics

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.

🎯 Your Challenge

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.

kotlin
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.

🎯 Your Challenge

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.

kotlin
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.

🎯 Your Challenge

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.

kotlin
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.)

✏️ Your Task

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.

Phase 10 of 12
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Predict the layout

Without running it, decide where the two texts appear on the screen.

kotlin
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.

🎯 Your Challenge

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.

kotlin
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:

kotlin
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.)

🎯 Your Challenge

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.

kotlin
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.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 12
Common Beginner Mistakes (and How to Fix Them)

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.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 12
Knowledge Check

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

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Market Stall Card

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:

  1. Create a new composable StallCard(modifier: Modifier = Modifier). Its outermost layout is a Box that fills the screen, has a soft orange background Color(0xFFFFF3E0) and centres its content.
  2. Inside, a Column that 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.
  3. At the top of the card, a Row with a 48 dp orange Icons.Default.Home icon, a 12 dp gap, and a small Column with the stall name (bold, 22 sp) and opening hours (grey, 14 sp). All vertically centred.
  4. A thin divider made from a Box that is 1 dp high, fills the width and is light grey, with space above and below it.
  5. Three menu rows made by one reusable private composable MenuRow(dish: String, price: String). The dish text takes the leftover width using weight(1f); the price is bold at the right.
  6. 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.

MainActivity.kt
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.

1

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.

2

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”.

3

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.

4

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.

5

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
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close