Introduction
Handling images efficiently in an Android application can be tricky, especially when it comes to fetching them from storage or capturing new ones. In this blog, we will explore how to read and write images using the Content Provider in Jetpack Compose while ensuring proper permission handling.
What is a Content Provider?
A Content Provider is an Android component that allows applications to share data in a structured way. It acts as a bridge between your app and external data sources, like media storage, databases, or files.
When dealing with images, Android stores them in MediaStore, and to access these images securely, we use the Content Resolver. This allows us to query, update, delete, or insert images efficiently.
Key Features of This Implementation
- Fetching images from storage using the Content Provider
- Handling permissions properly
- Loading images efficiently with paging
- Taking pictures using the camera and saving them to storage
- Displaying images in a grid format with Jetpack Compose
Step 1: Requesting Permissions
Required Permissions in AndroidManifest.xml
Before accessing images, we must declare the necessary permissions in AndroidManifest.xml
:
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Declare the camera feature but mark it as not required -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
READ_MEDIA_IMAGES
: Allows the app to access image files stored in the device.CAMERA
: Grants permission to capture images using the camera.uses-feature
: Specifies that the app can use a camera but does not require it.
Before accessing images, we need to request the necessary permissions. On Android 13 (API 33) and above, we need READ_MEDIA_IMAGES
, whereas older versions require READ_EXTERNAL_STORAGE
.
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
hasPermissions = permissions.values.all { it }
if (hasPermissions) {
loadImagesFromGallery(context, 40)
}
}
We check for existing permissions and request them if necessary:
LaunchedEffect(Unit) {
val permissions = arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.CAMERA
)
if (permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}) {
hasPermissions = true
loadImagesFromGallery(context, 40)
} else {
permissionLauncher.launch(permissions)
}
}
Step 2: Fetching Images from Storage
To retrieve images efficiently, we use ContentResolver.query()
to fetch images from MediaStore.
fun loadImagesFromGallery(context: Context, limit: Int, loadNew: Boolean = false) {
if (isLoading) return // Prevent multiple fetches
isLoading = true
scope.launch(Dispatchers.IO) {
val photos = mutableListOf<ImageItem>()
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME
)
val queryArgs = Bundle().apply {
putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
putInt(ContentResolver.QUERY_ARG_OFFSET, if (loadNew) 0 else offset)
}
context.contentResolver.query(collection, projection, queryArgs, null)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val displayName = cursor.getString(displayNameColumn)
val contentUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString())
try {
val bitmap = context.contentResolver.loadThumbnail(
contentUri, Size(200, 200), null
).asImageBitmap()
photos.add(ImageItem(id, displayName, contentUri, bitmap))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
if (photos.isNotEmpty()) {
if (loadNew) {
images.clear()
offset = 0
}
images.addAll(photos)
offset += limit
}
isLoading = false
}
}
Step 3: Displaying Images in a Grid
Once the images are fetched, we use Jetpack Compose’s LazyVerticalGrid
to display them.
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
items(images) { image ->
ImageCard(image) { selectedImage = image }
}
}
Each image is shown inside a Card with a click action:
@Composable
fun ImageCard(image: ImageItem, onClick: () -> Unit) {
Card(
modifier = Modifier.padding(4.dp).aspectRatio(1f)
) {
Image(
bitmap = image.bitmap,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize().clickable { onClick() }
)
}
}
Step 4: Capturing a New Image
To capture a new image, we create a Uri
using MediaStore
and launch the camera intent:
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success ->
if (success) {
loadImagesFromGallery(context, 40, true)
}
}
The function createImageUri()
generates a new image URI:
fun createImageUri(context: Context): Uri? {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
}
return context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
)
}
When the user clicks the camera button, we capture the image:
IconButton(onClick = {
if (hasPermissions) {
val uri = createImageUri(context)
uri?.let { cameraLauncher.launch(it) }
}
}) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
}
Key Concepts Explained
Data Class: ImageItem
Holds details about each image (ID, name, URI, and the thumbnail bitmap):
data class ImageItem(
val id: Long,
val name: String,
val uri: Uri,
val bitmap: ImageBitmap
)
Thumbnail Loading
Loading full-sized images can slow down the app. Instead, we load smaller thumbnails:
context.contentResolver.loadThumbnail(uri, Size(200, 200), null)
Why Background Threads?
Fetching images can block the UI. We use Dispatchers.IO
to run this task in the background:
scope.launch(Dispatchers.IO) {
// Fetch images here
}
Final Thoughts
This app demonstrates:
- Permission handling: Respecting user privacy.
- Content Providers: Safely accessing shared data.
- Efficient loading: Using thumbnails and pagination.
You can extend this by adding features like image deletion, filters, or cloud uploads. Remember to always test on different Android versions, as storage permissions vary!
Github gist for the code is here and tweak it to make it your own. Happy coding! 🚀