Read & Write Images Using Content Provider | Gallery App | Jetpack Compose | Kotlin

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top