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

} ```

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! 🚀