Images, icons and shapes
Icon, Image and Canvas are three different answers to "put a picture here", and picking the wrong one is why some apps are 40 MB. Plus the shapes that clip everything, and the one parameter that decides whether a screen reader can use your app.
Pocket Notes contains one picture file
Open the finished Pocket Notes project and look in res/drawable. There is a single file in there, and it is the launcher icon.
Yet the app has a plus symbol on its button, a pencil on its empty screen, a magnifying glass that appears when a search finds nothing, and rounded cards throughout. None of those is a picture. They are all drawn, at the moment they are needed, at exactly the size they are needed.
That is not a clever trick — it is the normal way to build an Android app, and it is most of the reason a well-built app is a few megabytes rather than forty.
Suppose you need a tree on a poster. You have three options in the room.
A photograph you glue on. It is exactly what it is: full colour, fixed size, and if you stretch it to fit a taller space the tree gets fat.
A rubber stamp of a tree. One shape, no colours of its own — it comes out in whatever ink you press it into, and a bigger stamp block gives a bigger tree with no loss of crispness.
A pen, and you draw the tree yourself. Total freedom, more work, and you can draw a tree nobody has ever stamped or photographed.
Compose gives you all three. Icon is the stamp, Image is the photograph, and is the pen. Nearly every mistake in this area is somebody reaching for the photograph when they wanted the stamp.
Icon — the stamp
1Icon(
2 imageVector = Icons.Default.Add,
3 contentDescription = "New note"
4)That is the plus on Pocket Notes' button. Three things about it are worth knowing:
- It is an — a shape described as lines and curves, so it is sharp at any size and weighs almost nothing.
- It has no colour of its own.
Iconpaints the whole thing in one tint, and unless you say otherwise that tint is the current content colour — which, inside aSurfaceor aButton, is already the correctoncolour from Lesson 3.11. Put an icon on a primary button and it comes outonPrimarywith no code. - It defaults to 24dp square, which is the Material standard. Change it with
Modifier.size(44.dp).
The icons themselves come from , in five styles:
| Style | Written as |
|---|---|
| Filled (the default) | Icons.Default.Add or Icons.Filled.Add |
| Outlined | Icons.Outlined.Add |
| Rounded | Icons.Rounded.Add |
| Sharp | Icons.Sharp.Add |
| Two-tone | Icons.TwoTone.Add |
Pick one style for a whole app. Mixed styles look like two people built it.
Material 3 ships with about forty icons built in — Add, Delete, Search, Menu, Close and the other everyday ones. Everything else lives in a separate library:
1implementation(
2 libs.androidx.material.icons.extended
3)Pocket Notes has that line, because Icons.Outlined.EditNote and Icons.Outlined.SearchOff are not in the core set. Dice Duel does not, because it needs none.
The extended library holds several thousand icons. In a strips the ones you never mention, so the shipped app pays for what it uses — but your debug APK does grow, and the build gets a little slower. Add it when you need it, not by habit.
contentDescription is not optional
The second parameter of every Icon and every Image is the one that decides whether somebody using can use your app at all.
The rule is one question: if the picture vanished, would information be lost?
1// Yes — the button has no other label.
2Icon(
3 imageVector = Icons.Default.Add,
4 contentDescription = "New note"
5)
6
7// No — the word "Roll" is right next to it.
8Icon(
9 imageVector = Icons.Default.Refresh,
10 contentDescription = null
11)null is a real answer, not a cop-out. It tells Android this is decoration, skip it, which is better than announcing "image" for the fiftieth time. What is never acceptable is describing the picture instead of its purpose: "plus icon" tells a blind user nothing. "New note" tells them everything.
Never include the word "button" or "icon" — the screen reader already says that part.
Image — the photograph
For an actual picture, from res/drawable:
1Image(
2 painter = painterResource(R.drawable.dice_hero),
3 contentDescription = null,
4 contentScale = ContentScale.Crop,
5 modifier = Modifier
6 .fillMaxWidth()
7 .height(180.dp)
8 .clip(RoundedCornerShape(16.dp))
9) turns a drawable — a PNG, a JPEG or a — into a , which is the thing Image knows how to draw. R.drawable.dice_hero is the same generated R class from Lesson 2.4, so a misspelled name is a compile error rather than a blank space.
is the parameter people forget, and it is the one that decides whether your photo looks professional or stretched:
| Value | What it does |
|---|---|
Crop | Fill the space, trim whatever overflows. Almost always what you want |
Fit | Fit the whole picture inside, leaving gaps |
FillBounds | Stretch to fill exactly. This is the squashed-face one |
Inside | Like Fit, but never scales a small image up |
FillBounds is the default-looking answer and is almost never right, because it ignores the picture's . If a photo of a person looks slightly wrong and you cannot say why, this is it.
Shapes
A shape in Compose is just a rule for cutting a corner. Three cover nearly everything:
1CircleShape
2RoundedCornerShape(16.dp)
3CutCornerShape(12.dp) is a full pill — on a square it is a circle, on a wide box it is a lozenge. takes one radius for all four corners, or one each: RoundedCornerShape(topStart = 20.dp, bottomEnd = 20.dp). slices the corner off flat instead of curving it.
Shapes turn up in four places, and they mean the same thing in all of them:
Modifier.clip(shape)— trim whatever is drawn after it in the chain (Lesson 3.3).Modifier.border(2.dp, color, shape)— draw an outline on that curve.Surface(shape = ...)andCard(shape = ...)— the Material way.MaterialTheme.shapes.medium— the shape scale from your theme, so cards across an app agree.
Surface — colour, shape and content colour together
is the Material container. Hand it a colour and a shape and it does three things: paints the background, clips its contents to the shape, and sets the content colour for everything inside so icons and text come out in the right on colour automatically.
Here is Pocket Notes' empty screen, which is nothing but a Surface, an Icon and two Texts.
1@Composable
2private fun EmptyMessage(
3 icon: ImageVector,
4 title: String,
5 body: String
6) {
7 val type = MaterialTheme.typography
8 val colors = MaterialTheme.colorScheme
9
10 Column(
11 modifier = Modifier
12 .fillMaxSize()
13 .padding(horizontal = 40.dp),
14 horizontalAlignment =
15 Alignment.CenterHorizontally,
16 verticalArrangement = Arrangement.Center
17 ) {
18 Surface(
19 shape = CircleShape,
20 color = colors.primaryContainer,
21 modifier = Modifier.size(96.dp)
22 ) {
23 Box(
24 contentAlignment = Alignment.Center
25 ) {
26 Icon(
27 imageVector = icon,
28 contentDescription = null,
29 tint = colors.onPrimaryContainer,
30 modifier = Modifier.size(44.dp)
31 )
32 }
33 }
34
35 Spacer(Modifier.height(24.dp))
36 Text(
37 text = title,
38 style = type.headlineSmall,
39 color = colors.onSurface
40 )
41 Spacer(Modifier.height(8.dp))
42 Text(
43 text = body,
44 style = type.bodyMedium,
45 color = colors.onSurfaceVariant,
46 textAlign = TextAlign.Center
47 )
48 }
49}EmptyNotes() in light mode. One Surface, one Icon, two Texts.
The pen: drawing it yourself
Sometimes the picture you need does not exist as an icon and would be silly as a photograph — a die showing whatever number was just rolled, for instance. Six PNGs would work, and would be six files that are wrong on some screen densities.
Dice Duel draws its die instead:
1Canvas(modifier = Modifier.size(160.dp)) {
2 val side = size.minDimension
3 drawRoundRect(
4 color = Color(0xFFFDFBFF),
5 size = Size(side, side),
6 cornerRadius = CornerRadius(
7 side * 0.22f,
8 side * 0.22f
9 )
10 )
11 drawCircle(
12 color = Color(0xFF241A4D),
13 radius = side * 0.085f,
14 center = Offset(side / 2f, side / 2f)
15 )
16}That is a die showing one — the real DieFace is the same two calls with a loop over pip positions around them. Inside a Canvas you are in a , which hands you size (the exact space you were given, in pixels) and a set of draw... functions. Every number is written as a fraction of side rather than a fixed measurement, which is why the same code produces a sharp 88dp die in the win banner and a 172dp one on the board.
Canvas gets a proper treatment in Part 5, where Focus Flow's ring dial is built on it.
So which one?
- The shape is a symbol with one colour →
Icon, from Material Icons. - It is a photograph or a multi-coloured graphic →
ImagewithpainterResource. - It changes with your data, or does not exist as a file →
Canvas.
If you are about to add a PNG of a symbol, stop and check the icon set first. It is almost always there, it will be sharper, and it will follow your theme colours for free.
- Open ComposeLab and open
MainActivity.kt. - Inside
setContent, add anIconwithimageVector = Icons.Default.AddandcontentDescription = "New note". Accept the imports forandroidx.compose.material.icons.Iconsandandroidx.compose.material.icons.filled.Add. - Tap Run. A small black plus, 24dp.
- Add
modifier = Modifier.size(64.dp)and Run again. Bigger, and still perfectly sharp — that is the vector doing its job. - Wrap the
Iconin aSurfacewithshape = CircleShapeandcolor = MaterialTheme.colorScheme.primaryContainer, plusmodifier = Modifier.size(96.dp). Run. The icon is in the top-left corner of the circle. - Put a
Box(contentAlignment = Alignment.Center)between theSurfaceand theIcon. Run. Centred. Remember that pairing — you will need it constantly. - Delete the
tintif you added one, and Run. The icon comes out inonPrimaryContainerwithout being told, becauseSurfaceset the content colour. - Change
CircleShapetoCutCornerShape(20.dp)and Run. Same code, an octagon. - Now try
Icons.Outlined.EditNote. It will not resolve — that icon lives in the extended library, and ComposeLab does not have it. That error is the third one in the Error Doctor below, and now you have seen it on purpose.
material package rather than material3 — the icons are shared between both versions of Material.import androidx.compose.material.icons.Icons, plus one import per icon you use: androidx.compose.material.icons.filled.Add for Icons.Default.Add. The Icon composable itself is separate again: androidx.compose.material3.Icon.implementation(libs.androidx.material.icons.extended) to app/build.gradle.kts, tap Sync, then import androidx.compose.material.icons.outlined.EditNote. If the library is already there, check the spelling and the style — Icons.Outlined.EditNote and Icons.Default.EditNote are different imports.import androidx.compose.ui.res.painterResource. It sits next to stringResource in androidx.compose.ui.res.res/drawable has a capital letter, a dash or a space in its name. Resource names become Kotlin identifiers in the R class, so the rules are strict.dice_hero.png, never DiceHero.png or dice-hero.png. Names must also start with a letter — 2x_logo.png fails too.Icon rather than Image. Icon exists to tint a single-colour symbol, so it repaints every pixel in the tint colour — which is exactly what it promises to do.Image(painter = painterResource(R.drawable.logo), contentDescription = ...). Keep Icon for one-colour symbols that should follow the theme.- Three ways to get a picture on screen:
Iconfor one-colour symbols,Imagefor real pictures, for anything you draw yourself. Icontakes an , defaults to 24dp, and paints everything in one — inside aSurfacethat tint is already the rightoncolour.- The core icon set is small. extended adds thousands, at the cost of a dependency; add it when you need it.
contentDescriptiondescribes the purpose, not the drawing. Usenullwhen the picture is decoration, and never say "button" or "icon".- loads a drawable; decides how it fills its box.
Cropis usually right,FillBoundssquashes. - ,
RoundedCornerShapeand are used identically byclip,border,SurfaceandCard. - paints, clips and sets the content colour in one composable. Wrap its child in a
BoxwithAlignment.Centerto centre it. - Next: animation — how to make the die actually roll, using the state you already have and four lines of code.