Capture the Moment: Mastering Photo Capture in Android Compose with CameraX

Ken Ruiz Inoue
Deuk
Published in
7 min readMar 2, 2024
Designed with DALL·E 3

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.

Environment

  • Android Studio Hedgehog | 2023.1.1 Patch 2
  • Compose version: androidx.compose:compose-bom:2023.08.00
  • Pixel 6 API 30 Emulator
  • Project Minimum SDK/API: 27

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
}
}

Step 3: BaseActivity - Managing Permissions

To ensure a smooth user experience, handling camera permissions efficiently is vital. Extend a base activity class to manage permission requests and checks there. Create BaseActivity.kt.

// Your package

import android.Manifest
import android.content.pm.PackageManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

// Open class allowing extension, so activities like MainActivity can inherit from it
// for common camera permission handling functionality
open class BaseActivity : ComponentActivity() {
// Key Point: Managing Camera Permission State
private val _isCameraPermissionGranted = MutableStateFlow(false)
val isCameraPermissionGranted: StateFlow<Boolean> = _isCameraPermissionGranted

// Declare a launcher for the camera permission request, handling the permission result
private val cameraPermissionRequestLauncher: ActivityResultLauncher<String> =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
// Permission granted, update the state
_isCameraPermissionGranted.value = true
} else {
// Permission denied: inform the user to enable it through settings
Toast.makeText(
this,
"Go to settings and enable camera permission to use this feature",
Toast.LENGTH_SHORT
).show()
}
}

// Checks camera permission and either starts the camera directly or requests permission
fun handleCameraPermission() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// Permission is already granted, update the state
_isCameraPermissionGranted.value = true
}

else -> {
// Permission is not granted: request it
cameraPermissionRequestLauncher.launch(Manifest.permission.CAMERA)
}
}
}
}

Step 4: Building the CameraScreen Composable

The CameraScreen composable function is the heart of our camera implementation. It orchestrates the camera preview, captures photos, and displays the captured image. Create CameraScreen.kt.


// Your package

import android.net.Uri
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.rememberAsyncImagePainter
// import YOUR_PACKAGE_NAME.CameraFileUtils.takePicture
import java.util.concurrent.Executors

@Composable
fun CameraScreen() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
// Executor for background tasks, specifically for taking pictures in this context
val executor = remember { Executors.newSingleThreadExecutor() }
// State to hold the URI of the captured image. Initially null, updated after image capture
val capturedImageUri = remember { mutableStateOf<Uri?>(null) }

// Camera controller tied to the lifecycle of this composable
val cameraController = remember {
LifecycleCameraController(context).apply {
bindToLifecycle(lifecycleOwner) // Binds the camera to the lifecycle of the lifecycleOwner
}
}

Box {
// PreviewView for the camera feed. Configured to fill the screen and display the camera output
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_START
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
controller = cameraController // Attach the lifecycle-aware camera controller.
}
},
onRelease = {
// Called when the PreviewView is removed from the composable hierarchy
cameraController.unbind() // Unbinds the camera to free up resources
}
)

// Button that triggers the image capture process
Button(
onClick = {
// Calls a utility function to take a picture, handling success and error scenarios
takePicture(cameraController, context, executor, { uri ->
capturedImageUri.value = uri // Update state with the URI of the captured image on success
}, { exception ->
// Error handling logic for image capture failures
})
},
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text(text = "Take Picture")
}

// Displays the captured image if available
capturedImageUri.value?.let { uri ->
Image(
// Asynchronously loads and displays the image from the URI
painter = rememberAsyncImagePainter(uri),
contentDescription = null,
modifier = Modifier
.width(80.dp)
.align(Alignment.BottomEnd),
contentScale = ContentScale.Crop
)
}
}
}

Step 5: Final Step, MainActivity Update

Finally, incorporate the camera functionality into your MainActivity, utilizing the camera permission logic from BaseActivity and displaying the CameraScreen based on permission status. Update MainActivity.kt.

// Your package

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

// MainActivity inherits from BaseActivity to utilize its camera permission handling logic
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Collect the camera permission state as a Compose state to automatically update the UI upon change
val permissionGranted = isCameraPermissionGranted.collectAsState().value

Box(modifier = Modifier.fillMaxSize()) {
// Conditional UI rendering based on camera permission state
if (permissionGranted) {
// If permission is granted, display the camera preview
CameraScreen()
} else {
// If permission is not granted, display a button to request camera permission
Button(
onClick = {
// Invoke the method from BaseActivity to handle permission request
handleCameraPermission()
},
modifier = Modifier.align(Alignment.Center)
) {
Text(text = "Start Capture")
}
}
}
}
}
}

Run your application to see the camera functionality in action. Upon granting camera permission, you capture photos and view them directly within the app.

More Resources

For developers eager to deepen their understanding or explore more about modern Android development, a wealth of resources and guides await.

Your thoughts and involvement are truly priceless. If you’ve found this guide helpful, I’d love to hear from you. Consider sharing your support through claps or a follow, and stay tuned for more insights into the evolving landscape of Android development. See you in the next tutorial!

Deuk Services: Your Gateway to Leading Android Innovation

Are you looking to boost your business with top-tier Android solutions?Partner with Deuk services and take your projects to unparalleled heights.

🚀 Boost Your Productivity with Notion

New to Notion? Discover how it can revolutionize your productivity

Ready to take your productivity to the next level? Integrate this content into your Notion workspace with ease:

1 Access the Notion Version of this Content

2 Look for the Duplicate button at the top-right corner of the page

3 Click on it to add this valuable resource to your Notion workspace

Seamlessly integrate this guide into your Notion workspace for easy access and swift reference. Leverage Notion AI to search and extract crucial insights, enhancing your productivity. Start curating your knowledge hub with Notion AI today and maximize every learning moment.

--

--