Android Image Loading Tutorial: Modern vs Legacy Approaches - 2025
Loading images from URIs in Android can be tricky, especially when supporting a wide range of API levels. If your app targets minSdk 21+ but wants to use the latest features on newer devices, you need a backward-compatible solution.
In this guide, we'll compare:
✅ Modern approach (API 28+): ImageDecoder
(better performance, more features)
✅ Legacy approach (API 21+): BitmapFactory
(works everywhere but less optimized)
Why Do We Need Different Approaches?
-
ImageDecoder
(API 28+) is the modern way, offering: -
Better memory handling
- Support for animated WebP/AVIF
-
Post-processing during decode
-
BitmapFactory
(API 1+) is the old way, but still reliable for older devices.
Problem: If you only use ImageDecoder
, your app will crash on Android 8.1 and below.
2. The Solution: Backward-Compatible Image Loading
Here’s how to safely load a bitmap from a Uri
while supporting all devices:
Step 1: Add Required Imports
kotlin
import android.graphics.BitmapFactory
import android.os.Build
import androidx.annotation.RequiresApi
Step 2: Load the Image Safely
```kotlin
fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Modern way (API 28+) loadWithImageDecoder(context.contentResolver, uri) } else { // Legacy way (API 21-27) loadWithBitmapFactory(context.contentResolver, uri) } }
@RequiresApi(Build.VERSION_CODES.P) private fun loadWithImageDecoder(contentResolver: ContentResolver, uri: Uri): Bitmap? { val source = ImageDecoder.createSource(contentResolver, uri) return ImageDecoder.decodeBitmap(source) }
private fun loadWithBitmapFactory(contentResolver: ContentResolver, uri: Uri): Bitmap? { return try { contentResolver.openInputStream(uri)?.use { stream -> BitmapFactory.decodeStream(stream) } } catch (e: Exception) { e.printStackTrace() null } } ```
Step 3: Usage Example
kotlin
val imageUri: Uri = ... // From gallery, file, or camera
val bitmap = loadBitmapFromUri(requireContext(), imageUri)
imageView.setImageBitmap(bitmap)
3. Key Takeaways
✔ Always check Build.VERSION.SDK_INT
before using newer APIs.
✔ Use try-catch
when dealing with file operations (URIs can fail!).
✔ Close streams properly with .use { }
to avoid memory leaks.
✔ Prefer ImageDecoder
on newer devices for better performance.
4. Bonus: Handling Large Images Efficiently
Both methods can lead to OutOfMemoryError
if the image is too large. To fix this:
Option 1: Downsample Before Loading
```kotlin private fun loadWithBitmapFactoryDownsampled( contentResolver: ContentResolver, uri: Uri, reqWidth: Int, reqHeight: Int ): Bitmap? { return contentResolver.openInputStream(uri)?.use { stream -> val options = BitmapFactory.Options().apply { inJustDecodeBounds = true BitmapFactory.decodeStream(stream, null, this) stream.reset() // Rewind stream to read again inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) inJustDecodeBounds = false } BitmapFactory.decodeStream(stream, null, options) } }
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val (height, width) = options.run { outHeight to outWidth } var inSampleSize = 1
if (height > reqHeight || width > reqWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize
} ```
Option 2: Use a Library (Recommended for Production)
Libraries like Glide or Coil handle all this complexity for you:
```kotlin // Using Coil (Kotlin-first) imageView.load(imageUri)
// Using Glide Glide.with(context).load(imageUri).into(imageView) ```
Final Thoughts
- For simple cases: Use the backward-compatible code above.
- For production apps: Use Glide/Coil—they handle caching, memory, and threading automatically.
Hope this helps! 🚀