photo.png

Objective

Learn how to implement a photo capture feature in an Android Compose app using CameraX, and display the captured photo.

Before You Start

For a simpler approach focusing solely on displaying the camera preview, refer to my previous guide.

From Setup to Preview: CameraX Integration in Jetpack Compose

Environment

Step 1: Project Setup

Begin by creating a new Compose project in Android Studio. To integrate CameraX and image loading capabilities, include the following dependencies in your build.gradle.kt file.

implementation("io.coil-kt:coil-compose:2.1.0")
implementation("androidx.camera:camera-core:1.3.1")
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")

Additionally, ensure you have the necessary camera permission and feature declared in your AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="<http://schemas.android.com/apk/res/android>"
    xmlns:tools="<http://schemas.android.com/tools>">

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" android:required="false" />

...

Step 2: Implementing CameraFileUtils

The CameraFileUtils object is critical in managing file operations for the camera functionality. It includes a method to trigger the photo capture process using CameraX. Create CameraFileUtils.kt.

// Your package

import android.content.Context
import android.net.Uri
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService

// Utility object for handling camera file operations
object CameraFileUtils {

    // Function to initiate the picture taking process
    fun takePicture(
        cameraController: CameraController, // CameraX's camera controller
        context: Context, // Application context
        executor: ExecutorService, // Executor service for running camera operations
        onImageCaptured: (Uri) -> Unit, // Callback for successful capture
        onError: (ImageCaptureException) -> Unit // Callback for errors during capture
    ) {
        // Create a file to save the photo
        val photoFile = createPhotoFile(context)
        // Prepare the output file options for the ImageCapture use case
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        // Instruct the cameraController to take a picture
        cameraController.takePicture(
            outputOptions,
            executor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    // On successful capture, invoke callback with the Uri of the saved file
                    Uri.fromFile(photoFile).let(onImageCaptured)
                }

                override fun onError(exception: ImageCaptureException) {
                    // On error, invoke the error callback with the encountered exception
                    onError(exception)
                }
            }
        )
    }

    // Helper function to create a file in the external storage directory for the photo
    private fun createPhotoFile(context: Context): File {
        // Obtain the directory for saving the photo
        val outputDirectory = getOutputDirectory(context)
        // Create a new file in the output directory with a unique name
        return File(outputDirectory, photoFileName()).apply {
            // Ensure the file's parent directory exists
            parentFile?.mkdirs()
        }
    }

    // Generates a unique file name for the photo based on the current timestamp
    private fun photoFileName() =
        SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
            .format(System.currentTimeMillis()) + ".jpg"

    // Determines the best directory for saving the photo, preferring external but falling back to internal storage
    private fun getOutputDirectory(context: Context): File {
        // Attempt to use the app-specific external storage directory which does not require permissions
        val mediaDir = context.getExternalFilesDir(null)?.let {
            File(it, context.resources.getString(R.string.app_name)).apply { mkdirs() }
        }
        // Fallback to internal storage if the external directory is not available
        return if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir
    }
}