Goals & prerequisites
An image can be perfectly loaded and still look wrong: the subject is cropped, the logo is distorted, or a decorative icon is announced three times. Learn to decide what an image means, where it comes from and how it occupies its bounds.
Complete Images, Icons & Resources first. You should recognize Modifier and Android's res directory. The worked example uses Column as a simple wrapper that places its children vertically; the upcoming layout lessons explain its arrangement and spacing in detail. This lesson uses a local vector supplied below, so every step works without downloading a photograph or adding an image-loading library.
- Choose between
ImageandIconfor the intended job. - Separate source dimensions, layout bounds, scaling and clipping.
- Build an offline illustration panel with accurate semantics.
- Diagnose missing resources, unintended tint and unexpected crop.
Image or Icon?
Use Image for a photograph, illustration, cover or other visual content. Use Icon for a compact symbol that communicates a state or action. Their names describe design intent; an icon still needs a surrounding interactive component such as IconButton when the user should activate it.
Icon normally tints its painter using the current content color. That is useful for a monochrome search symbol and surprising for a multicolor brand mark. Choose Image to preserve visual content, or deliberately use Color.Unspecified when preserving a painter's colors inside an icon is appropriate.
Create the local asset
Create app/src/main/res/drawable/deep_compose_landscape.xml with this complete vector. Use lowercase letters and underscores in resource names. A vector describes geometry that scales cleanly; it is suitable for this simple illustration, while a detailed photo belongs in a raster format.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="240dp" android:height="120dp"
android:viewportWidth="240" android:viewportHeight="120">
<path android:fillColor="#DDF1F0"
android:pathData="M0,0H240V120H0Z" />
<path android:fillColor="#F7B955"
android:pathData="M174,20H202V48H174Z" />
<path android:fillColor="#3B8B80"
android:pathData="M0,120L76,28L146,120Z" />
<path android:fillColor="#245D65"
android:pathData="M82,120L174,49L240,120Z" />
</vector>Add these entries to the existing strings.xml resource element. Use the complete wrapper only when creating a separate values file. Keeping the description in resources makes it available to localization along with the visible copy.
<resources>
<string name="deep_image_description">Two green mountains beneath a golden square sun</string>
<string name="deep_image_title">A quiet place to learn</string>
<string name="deep_image_caption">A local vector illustration, available offline.</string>
</resources>Painter, bounds & scale
| Input | Responsibility | Question to ask |
|---|---|---|
painter | Supplies the drawable content | Which local resource or loaded result should be drawn? |
modifier | Sets the image's measured space and decorations | Should the container be square, wide, fixed or flexible? |
contentScale | Maps source proportions into those bounds | May the edges be cropped, or must every part remain? |
alignment | Positions scaled content within available bounds | Which region matters when the source does not match the frame? |
contentDescription | Exposes useful visual meaning to accessibility services | Does this image add information beyond adjacent text? |
Read an image call in that order. Changing contentScale does not request a larger network image or change your resource file. Changing the modifier's aspect ratio does not rewrite the original artwork. Those are separate decisions with different consequences.
The main Image overloads accept a Painter, an ImageBitmap, or an ImageVector. Choose the input that you already have: painterResource is convenient for a packaged drawable, a decoded bitmap is pixel content, and an image vector describes vector geometry. Do not manually decode a bitmap on every recomposition to force it into a different overload.
Crop, Fit & alignment
ContentScale.Fit preserves proportions and keeps the whole image within its bounds; unused space can remain. ContentScale.Crop preserves proportions while filling the frame, so some source content may disappear. FillBounds stretches the source independently in each direction and can distort it. Pick the behavior from the content's purpose.
Our source is twice as wide as it is tall. Put it in a square with Fit and you can still see both mountains and the sun. Switch to Crop and the square keeps only part of that wide scene. Alignment lets you favor a region; it cannot recover everything while also filling an incompatible frame.
Clipping, borders & order
A shape is another concern. clip(CircleShape) changes the visible outline, while Crop decides how the source fits behind that outline. Neither replaces the other. For an avatar, specify a square size before a circular clip so the intention is clear.
// Focused modifier chain: an outer ring, a gap, then clipped content.
Modifier
.size(96.dp)
.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape)
.padding(4.dp)
.clip(CircleShape)
// For a rounded rectangular panel, give border and clip the same shape:
Modifier
.fillMaxWidth()
.aspectRatio(2f)
.clip(RoundedCornerShape(16.dp))In the first chain, the border encloses the outer 96 dp area, padding leaves an interior gap, and the inner image is clipped. Move the padding outside the border and the boundary changes. Draw the intended boxes on paper when a border looks offset; random changes to the artwork will not correct a modifier-order problem.
Build an illustration panel
Create IllustrationPanel.kt in the same package as your generated MainActivity. Keep its package declaration and paste the following imports and composable. Inside the existing setContent and generated app theme, replace the greeting with IllustrationPanel(). This uses Material 3 and the two resource files above.
// Keep the existing package declaration; use the same package as MainActivity.
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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 IllustrationPanel(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth().padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Image(
painter = painterResource(R.drawable.deep_compose_landscape),
contentDescription = stringResource(R.string.deep_image_description),
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f)
.clip(RoundedCornerShape(16.dp)),
)
Text(
text = stringResource(R.string.deep_image_title),
style = MaterialTheme.typography.titleLarge,
)
Text(
text = stringResource(R.string.deep_image_caption),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}The complete scene should fill a wide rounded panel without distortion. Text should wrap naturally beneath it. The image has its own description because the heading does not describe the scene. If the image were only ornamental behind a heading that conveys all relevant meaning, null would be the appropriate description instead.
Explore image scaling
Compare fit and crop before changing the Android code. The browser preview illustrates geometry; it does not run Compose or validate Android accessibility behavior. Confirm the actual panel with the local vector on your emulator.
One image, different bounds
"Two green mountains beneath a golden square sun".Image(
painter = painterResource(R.drawable.deep_compose_landscape),
contentDescription = stringResource(R.string.deep_image_description),
contentScale = ContentScale.Crop,
modifier = Modifier
.width(240.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(24.dp)),
)Interactive browser model with a Kotlin configuration excerpt. It does not run Compose. Font metrics, scaling, touch targets, focus, and assistive technology must be checked on an Android device or emulator.
Meaning & accessibility
Describe useful meaning rather than filenames or obvious phrases such as “image of.” A product diagram needs its meaningful difference; a purely decorative flourish should have contentDescription = null. An empty string is not a clear substitute for explicitly marking a decoration.
For an action icon, describe the action, such as “Add bookmark,” and update that wording when the action becomes “Remove bookmark.” Avoid a duplicate description on both the button and its child. A tiny glyph can live inside a suitably large button; increasing only the glyph does not create a clear interaction contract.
// Additional imports: material3.Icon, ui.graphics.Color,
// foundation.layout.size, ui.res.painterResource, ui.unit.dp.
// Inside a @Composable function:
Icon(
painter = painterResource(R.drawable.deep_compose_landscape),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.size(32.dp),
)
// This preserves the asset's colors but is decorative here.
// Prefer Image for this multicolor illustration in the actual panel.
// For a monochrome action glyph, Icon's theme-aware tint is usually useful.Loading & troubleshooting
painterResource reads supported drawable resources packaged with the app; it does not fetch an HTTPS URL. Remote images need an image-loading solution and explicit loading, success and failure behavior. Keep a stable frame while loading so nearby controls do not jump. This exercise stays local; do not add an unrelated dependency merely to display its vector.
| Symptom | Likely cause | Check |
|---|---|---|
| R.drawable entry cannot resolve | Wrong directory/name or invalid XML | Confirm the exact drawable filename, XML syntax and app R import. |
| All colors become one color | Icon applies its default tint | Use Image or intentionally preserve colors with Color.Unspecified. |
| Subject disappears at the edge | Crop with an incompatible frame | Compare Fit, frame aspect ratio and alignment. |
| Rounded corners still look square | Background or sibling artwork lies outside the clip | Inspect which composable actually owns the visible pixels. |
Test a failed assumption
Practice & worked solution
- Run the wide panel and identify the sun and both mountains.
- Change the frame to square. Compare
FitandCropwithout changing the source. - Keep
Cropand choose an alignment that retains the right-hand sun. Explain the part of the scene you lose. - Read the panel with TalkBack and increase the system font size. Confirm the caption can grow independently of the image.
Acceptance checks: the artwork never stretches, the crop is intentional, there are no unresolved resources, and the meaningful image is announced once. A decorative variation should be skipped while its visible caption is still read.
Reveal the square-crop solution
The source is wide, so the square crop discards horizontal content. Choosing the end region keeps the sun; test the center alignment alongside it to explain the difference.
// Add: import androidx.compose.ui.Alignment
// Replace the Image call inside IllustrationPanel:
Image(
painter = painterResource(R.drawable.deep_compose_landscape),
contentDescription = stringResource(R.string.deep_image_description),
contentScale = ContentScale.Crop,
alignment = Alignment.TopEnd,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)),
)
// Compare Alignment.Center and Alignment.TopEnd on this wide source.
// Crop preserves proportions while choosing which horizontal region remains.Quick check & references
Key takeaways
- Choose Image for visual content and Icon for symbols with intentional tint.
- Source, bounds, scale, alignment and clipping solve different problems.
- Fit retains the full image; Crop fills the frame and may discard edges.
- Describe meaningful visuals and mark decorations with a null description.
Quick check
1. A wide logo must remain completely visible inside a square. Which starting point is appropriate?
2. Why did a multicolor drawable turn monochrome inside Icon?
3. A flourish repeats information already conveyed by nearby text. What description should it have?
Read the official guides to image customization, image loading, resources, and accessibility defaults.