Phase 1 · Getting Started with ComposeModule 10~45 min read

Image & Icon: Resources, Scaling & Semantics

Load local assets, compare Crop and Fit, shape images, control icon tint and write meaningful descriptions. Experiment with image bounds in a visual lab.

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 Image and Icon for 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.

res/drawable/deep_compose_landscape.xml
<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.

res/values/strings.xml
<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

InputResponsibilityQuestion to ask
painterSupplies the drawable contentWhich local resource or loaded result should be drawn?
modifierSets the image's measured space and decorationsShould the container be square, wide, fixed or flexible?
contentScaleMaps source proportions into those boundsMay the edges be cropped, or must every part remain?
alignmentPositions scaled content within available boundsWhich region matters when the source does not match the frame?
contentDescriptionExposes useful visual meaning to accessibility servicesDoes 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.

ImageBoundaries.kt
// 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))
Focused modifier chains. Import border, CircleShape, RoundedCornerShape, clip, aspectRatio, size and padding from their Compose packages.

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.

IllustrationPanel.kt
// 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,
        )
    }
}
Complete worked composable with explicit imports; no extra dependency or permission is required.

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.

Composable lab · Change & observe

One image, different bounds

Source aspect ratio · 2:1
Two green mountains beneath a golden square sun
Crop fills the bounds and can cut off the edges. Content description: "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.

IconTint.kt
// 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.
Focused comparison using the supplied asset; the complete panel intentionally uses Image.

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.

SymptomLikely causeCheck
R.drawable entry cannot resolveWrong directory/name or invalid XMLConfirm the exact drawable filename, XML syntax and app R import.
All colors become one colorIcon applies its default tintUse Image or intentionally preserve colors with Color.Unspecified.
Subject disappears at the edgeCrop with an incompatible frameCompare Fit, frame aspect ratio and alignment.
Rounded corners still look squareBackground or sibling artwork lies outside the clipInspect which composable actually owns the visible pixels.

Test a failed assumption

A vector that looks sharp proves only that this asset scales well. It does not prove a large bitmap is inexpensive to decode or that a remote request will succeed. Revisit loading and memory decisions when the source changes.

Practice & worked solution

  1. Run the wide panel and identify the sun and both mountains.
  2. Change the frame to square. Compare Fit and Crop without changing the source.
  3. Keep Crop and choose an alignment that retains the right-hand sun. Explain the part of the scene you lose.
  4. 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.

IllustrationPanel-square.kt
// 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.