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:idfor view binding accessandroid:layout_width/layout_heightfor the boundsandroid:srcfor the static drawable (use@drawable/name)android:contentDescriptionfor screen reader accessibilityandroid:scaleTypefor 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 drawable | PNG/WebP bitmap | |
|---|---|---|
| Best for | Icons, logos, simple illustrations | Photos, complex artwork |
| File size | Small (XML) | Larger, scales with resolution |
| Resolution-independent | Yes | No, need multiple sizes |
| Rendering cost | Higher (path drawing per render) | Lower (cached bitmap blit) |
| Tint support | Easy via android:tint | Needs 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.
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.
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:
-
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. -
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
setImageBitmapdoes not. -
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.
-
Using fitXY out of laziness. Stretches the image, looks bad, easy to fix. The right call is almost always
centerCrop(fill the box) orcenterInside(show the whole thing). -
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.
Related
- Android Kotlin Introduction - the language basics behind the snippets
- Button OnClick in Android Kotlin - common pairing, tap an image to open something
- EditText with Toast in Android Kotlin - input handling, often combined with image upload flows
- Android Introduction - the broader platform and project layout context