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
import android.graphics.BitmapFactory
import android.os.Build
import androidx.annotation.RequiresApi
Step 2: Load the Image Safely
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
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
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:
// 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! π