Skip to content

ImageView in Android Kotlin: A Practical Introduction

Display images in Android Kotlin apps. XML attributes, scaleType, drawable loading, network images with Coil or Glide, accessibility, and aspect ratio.

· · 7 min read
Android app screen showing a photo gallery grid with three ImageView thumbnails at different aspect ratios

Quick Take

ImageView is how every Android app shows photos, icons, and logos. This guide covers the XML attributes that matter, the eight scaleType values, loading network images with Coil, and the accessibility settings that keep your app usable for screen reader users.

Quick take: ImageView displays drawables, bitmaps, and remote images in Android. Set src in XML for static images, use scaleType to control how the image fits the bounds, and reach for Coil (Kotlin-first) or Glide (more battle-tested) for network loading. Always set contentDescription or mark the view as not-important-for-accessibility, screen reader users depend on it, with practical examples.

ImageView is the widget that displays any image on Android. The framework version has been around since API 1, and despite a couple of decades of evolution, the API is still mostly the same: load an image into the view, pick a scaleType, set a content description, and you're done. The interesting bits are around loading, network images, large bitmaps, and lifecycle-aware caches, all of which the standard library doesn't handle, which is why every modern Android app uses Coil or Glide.

I'll walk through the XML, the scaleType options that come up most, network loading with Coil, and the accessibility settings that matter. Then a couple of patterns I wish someone had shown me earlier.

How Do You Add an ImageView in XML?

The simplest ImageView for a static drawable shipped with your app:

<ImageView
    android:id="@+id/logo_image"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:src="@drawable/logo"
    android:contentDescription="@string/logo_description"
    android:scaleType="centerInside" />

Five attributes worth setting:

  • android:id for view binding access
  • android:layout_width / layout_height for the bounds
  • android:src for the static drawable (use @drawable/name)
  • android:contentDescription for screen reader accessibility
  • android:scaleType for how the image fits inside the bounds

@drawable/logo references a file at res/drawable/logo.png or res/drawable/logo.xml (vector). Android handles density buckets automatically if you provide multiple resolutions in drawable-mdpi, drawable-hdpi, drawable-xhdpi, etc. For most new projects in 2026, a single vector drawable in drawable/ is the right choice and scales to any density without quality loss.

Vector vs Bitmap

A quick comparison of when to use which:

Vector drawablePNG/WebP bitmap
Best forIcons, logos, simple illustrationsPhotos, complex artwork
File sizeSmall (XML)Larger, scales with resolution
Resolution-independentYesNo, need multiple sizes
Rendering costHigher (path drawing per render)Lower (cached bitmap blit)
Tint supportEasy via android:tintNeeds ColorFilter setup

I keep vector drawables for everything UI-chrome (toolbar icons, navigation icons, empty-state graphics) and PNG or WebP for photos. Vector is a bad fit for anything with photographic detail because the file size balloons fast and rendering cost goes with complexity.

What Does scaleType Actually Do?

scaleType controls how the source image fits inside the ImageView's bounds when they don't match the image's natural aspect ratio. Eight values, six worth knowing:

  • center - centers the image at original size, no scaling. Crops if the image is bigger than the view.
  • centerCrop - scales until both axes are at least as big as the view, then centers. Crops the overflow. This is what you want for profile pictures and hero images.
  • centerInside - scales down to fit if needed, never scales up. Centers. Good for logos that should look crisp.
  • fitCenter - scales to fit inside the bounds (no crop), centered. Both axes touch the bounds. The default.
  • fitXY - stretches both axes to fill, ignoring aspect ratio. Almost always wrong, looks distorted.
  • matrix - lets you provide a custom transformation matrix. Rare, used for pan-zoom widgets.

The two that matter day to day: centerCrop for "fill the box, crop if needed" and fitCenter for "show the whole image, letterbox if needed". I get the choice wrong about once per project, the symptom is either a stretched photo or weird empty bars on the sides.

A hand holding a phone that shows a scrollable grid of photo thumbnails
Photo by Plann on Unsplash

How Do You Load Images at Runtime?

Static drawables from XML are fine, but most real apps need to set images at runtime, profile pictures from a server, photo gallery thumbnails, dynamic icons based on data. With view binding enabled:

import com.example.myapp.R
import com.example.myapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Load a drawable resource
        binding.logoImage.setImageResource(R.drawable.logo)

        // Or set a Drawable
        binding.logoImage.setImageDrawable(getDrawable(R.drawable.logo))
    }
}

For network images, don't roll your own. The boilerplate is painful (URL connection, background thread, bitmap decoding, memory cache, disk cache, error states, retries, view recycling in lists) and you'll get it wrong. Use Coil.

How Do You Load Network Images with Coil?

Coil is the Kotlin-first image loading library from the same team that maintains OkHttp. Add it to your module-level build.gradle.kts:

dependencies {
    implementation("io.coil-kt:coil:2.6.0")
}

Then loading a URL is one line:

import coil.load

binding.profileImage.load("https://example.com/avatar.webp")

.load is a Coil extension function on ImageView. It handles the background fetch, decoding, memory and disk caching, and the bitmap assignment. If the activity is destroyed mid-load, Coil cancels the request automatically.

For real use cases, you'll want a placeholder, an error fallback, and probably a transformation:

import coil.load
import coil.transform.CircleCropTransformation

binding.profileImage.load("https://example.com/avatar.webp") {
    placeholder(R.drawable.avatar_placeholder)
    error(R.drawable.avatar_error)
    crossfade(true)
    transformations(CircleCropTransformation())
}

That gives you a placeholder while loading, an error drawable on failure, a 300ms crossfade animation when the real image arrives, and a circle crop applied to the result. The Coil docs cover the full builder API.

Glide is the older alternative and still totally fine. The syntax is similar:

import com.bumptech.glide.Glide

Glide.with(this)
    .load("https://example.com/avatar.webp")
    .placeholder(R.drawable.avatar_placeholder)
    .into(binding.profileImage)

For new Kotlin projects, I lean Coil. Smaller binary size, coroutine-native, and the API reads more naturally with Kotlin extension functions.

A phone camera framing a mountain lake, the scene visible on the screen
Photo by Josh Power on Unsplash

How Do You Handle Accessibility?

ImageView accessibility is one line of XML, and the most-skipped one. Every meaningful image needs a contentDescription so screen readers (TalkBack on Android) can announce what the image represents.

<ImageView
    android:src="@drawable/profile_photo"
    android:contentDescription="Profile photo of user Alice" />

For decorative images that add no information (a background pattern, a divider, a corner ornament), mark them as unimportant:

<ImageView
    android:src="@drawable/divider_swirl"
    android:importantForAccessibility="no" />

The accessibility scanner from Google's Play Pre-launch Report flags missing content descriptions, and the Lint warning shows up in Android Studio at build time. Don't ignore those, real users depend on the labels.

A short note on quality: write the description as a meaningful sentence, not a literal description of pixels. "Alice's profile photo" beats "A circular photo of a smiling woman in a blue shirt". The screen reader user wants to know what the image means in context, not what it looks like.

What Are the Common Pitfalls?

A handful of patterns I've seen burn time on real projects:

  1. wrap_content on both axes with a network image. The view starts at 0 by 0 and pops to full size when the image loads, causing a visible jolt. Fix: set explicit dp dimensions or use android:adjustViewBounds="true" with one fixed axis.

  2. Decoding huge bitmaps directly. A 4000 by 3000 photo from a phone camera is 48 MB uncompressed in memory. Setting it directly will OOM on lower-end devices. Coil and Glide handle the downsampling automatically, plain setImageBitmap does not.

  3. Forgetting contentDescription. TalkBack users hit a wall of "image, image, image" announcements. Adds zero value, often makes apps unusable. Lint warns about this for a reason.

  4. Using fitXY out of laziness. Stretches the image, looks bad, easy to fix. The right call is almost always centerCrop (fill the box) or centerInside (show the whole thing).

  5. Loading images on the main thread. Drawable resources are fine. Anything from disk, network, or large bitmap decoding has to go off-main-thread. Coroutines, Coil, or Glide solve this. Don't reinvent.

Honestly, once Coil is in your project, network image loading becomes a non-issue. The work shifts to picking the right scaleType, getting the bounds right, and writing decent contentDescriptions. Those are 80% of what makes ImageView use look polished.

Frequently Asked Questions

What's the difference between Coil and Glide?
Coil is Kotlin-first, uses coroutines, and is about 50% smaller than Glide (around 1500 methods vs 2700 according to Coil's own benchmarks). Glide is the older, battle-tested option with more configurability and broader format support. For new Kotlin projects, I default to Coil. For legacy Java projects or apps that need GIF and WebP support out of the box, Glide is still a fine choice.
Which scaleType should I use most often?
centerCrop for photos that should fill a fixed-size container (profile pictures, hero images). fitCenter when you want the whole image visible without cropping (logos, icons in tiles). The default is fitCenter, so leaving scaleType unset usually does the right thing for small UI images and breaks for large hero images.
How do I avoid the ImageView jumping when the image loads?
Set both layout_width and layout_height to fixed dp values, or use a fixed aspect ratio via android:adjustViewBounds="true" plus a maxWidth. The worst pattern is wrap_content on both axes, the ImageView starts at 0 by 0 and pops to the image's size when loading finishes, causing a visible layout jump.
Do I need to load images on a background thread?
For drawables packaged with the app, no, they're loaded fast enough. For network images or large files from storage, yes, and that's exactly what Coil and Glide handle for you. Never call setImageBitmap on the main thread with a freshly-decoded large bitmap, you'll see frame drops and ANRs on slower devices.
What contentDescription should I set?
A short sentence describing the image's meaning, not its appearance. For a profile picture, '[Username]'s profile photo' is better than 'A circular photo of a person smiling'. For purely decorative images, set android:importantForAccessibility="no" instead, screen readers skip those entirely.