Skip to main content

Android Camera Integration with Kotlin: A Step-by-Step Guide

· 16 min read
Engineering Team

1. Project Configuration

Follow these steps to set up your Android Studio project:

  1. Open Android Studio (latest version recommended)
  2. Create a new project with the "Empty Compose Activity" template
  3. Configure your project with these settings:
    • Language: Kotlin
    • Minimum SDK: API 21 (Android 5.0) or higher
    • Build configuration: Gradle with Kotlin DSL (build.gradle.kts)

2. Required Camera Permissions

Add these essential permissions to your AndroidManifest.xml file:

<!-- Camera permission -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Ensures your app only shows on devices with a camera -->
<uses-feature android:name="android.hardware.camera.any"/>
<!-- Required for video recording -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!-- Optional: for saving media to external storage (Android 10+) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- For Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

Note: You'll implement runtime permission handling later in this tutorial using the Accompanist Permissions library.

3. CameraX DependencieseraX and Jetpack Compose (2025)

android camera integration kotlin step step guide

Are you struggling with camera implementation in your Android app? In this comprehensive tutorial, you'll master Android camera integration using the powerful CameraX API with Kotlin and Jetpack Compose. Whether you're building a social media app, a document scanner, or implementing AR features, this step-by-step guide covers everything you need to know.

By the end of this tutorial, you'll know how to:

  • Create a professional camera preview in your Compose UI
  • Capture high-quality photos and videos efficiently
  • Implement machine learning features with ML Kit integration
  • Apply advanced camera extensions like HDR, Night Mode, and Portrait/Bokeh effects
  • Handle camera lifecycles and errors like a professional developer

Difficulty level: Intermediate Prerequisites: Basic knowledge of Kotlin and Jetpack Compose Android API compatibility: Android 5.0 (API 21) and above


🔧 Why Choose CameraX for Your Android App?

Modern Android camera development has historically been complicated, with inconsistent behavior across different device manufacturers. The CameraX API solves these problems and delivers these key advantages:

  • Cross-device compatibility: Works consistently on 90%+ of Android devices (API 21/Android 5.0 and higher)
  • Lifecycle awareness: Camera resources are automatically managed based on your app's lifecycle
  • Use-case driven API: Ready-to-use components for Preview, ImageCapture, VideoCapture, and ImageAnalysis
  • Compose integration: Seamless integration with modern Jetpack Compose UI
  • ML Kit compatibility: Built-in support for machine learning use cases (barcode scanning, face detection, etc.)
  • Advanced photography: Access to vendor extensions like HDR, Night mode, and Portrait/Bokeh effects

"CameraX is the recommended camera library for most Android developers today." - Android Developer Documentation


📦 Setting Up Your CameraX Project

1. Project Setup

Create a Kotlin project in Android Studio with Jetpack Compose enabled and minSdk 21 or higher.

2. Permissions

In AndroidManifest.xml:

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

You’ll also need runtime permission handling in your app logic (via Accompanist Permissions or standard checks).

3. Dependencies

Add the following dependencies to your build.gradle.kts (app module):

// Define CameraX version - use the latest stable version
val cameraxVersion = "1.4.2"

dependencies {
// Core CameraX libraries
implementation("androidx.camera:camera-core:$cameraxVersion")
implementation("androidx.camera:camera-camera2:$cameraxVersion")
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
implementation("androidx.camera:camera-view:$cameraxVersion")

// For video capture functionality
implementation("androidx.camera:camera-video:$cameraxVersion")

// For ML Kit integration
implementation("androidx.camera:camera-mlkit-vision:$cameraxVersion")

// For HDR, Night Mode, Portrait features
implementation("androidx.camera:camera-extensions:$cameraxVersion")

// For Jetpack Compose integration
implementation("androidx.camera:camera-compose:$cameraxVersion")

// For permission handling (optional but recommended)
implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha")
}

After adding these dependencies, sync your project to ensure everything is correctly set up.


📷 Building a Camera Preview with Image Capture in Compose

Let's start with the most common use case: displaying a live camera preview and capturing photos when the user taps a button.

Here's the complete implementation of a camera screen with photo capture functionality:

import android.net.Uri
import android.util.Log
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 androidx.core.content.ContextCompat
import java.io.File
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
* A full-screen camera implementation with image capture functionality
*
* @param onImageCaptured Callback triggered when an image is successfully captured
* @param onError Callback triggered on camera errors
*/
@Composable
fun CameraScreen(
onImageCaptured: (Uri) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
// Camera state
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
val cameraSelector = remember(lensFacing) {
CameraSelector.Builder().requireLensFacing(lensFacing).build()
}

// Initialize CameraX use cases
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }

// Configure image capture with high-quality settings
val imageCapture = remember {
ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
.build()
}

// Camera preview using AndroidView to integrate with Compose
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { ctx ->
// Configure preview view
val previewView = PreviewView(ctx).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
scaleType = PreviewView.ScaleType.FILL_CENTER
}

// Setup and bind use cases when camera provider is available
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()

// Configure preview use case
val preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}

try {
// Unbind previous use cases before rebinding
cameraProvider.unbindAll()

// Bind camera to lifecycle
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
} catch (e: Exception) {
Log.e("CameraScreen", "Camera binding failed", e)
}
}, ContextCompat.getMainExecutor(ctx))

previewView
},
modifier = Modifier.fillMaxSize()
)

// Camera controls overlay
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Camera flip button
FloatingActionButton(
onClick = {
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK)
CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK
}
) {
Icon(Icons.Default.FlipCameraAndroid, contentDescription = "Switch camera")
}

// Capture button
FloatingActionButton(
onClick = {
// Create output file in cache directory
val photoFile = File(
context.cacheDir,
"IMG_${System.currentTimeMillis()}.jpg"
)

// Configure output options with metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(
ImageCapture.Metadata().apply {
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
).build()

// Capture the image
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
// Return the image URI to the caller
output.savedUri?.let { uri ->
onImageCaptured(uri)
} ?: onImageCaptured(Uri.fromFile(photoFile))
}

override fun onError(exception: ImageCaptureException) {
Log.e("CameraScreen", "Image capture failed", exception)
onError(exception)
}
}
)
},
modifier = Modifier.size(72.dp)
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = "Take photo",
modifier = Modifier.size(36.dp)
)
}
}

Spacer(modifier = Modifier.height(36.dp))
}
}
}

🎥 Implementing Video Recording with CameraX

Recording video is another essential feature in many camera applications. Here's how to add professional video recording capability to your app:

Creating a Video Recording Composable

import android.Manifest
import android.content.ContentValues
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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 androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executor

@Composable
fun VideoRecordingScreen(
onVideoSaved: (Uri) -> Unit,
onError: (String) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()

// State for recording
var isRecording by remember { mutableStateOf(false) }
var recording: Recording? by remember { mutableStateOf(null) }

// Date formatter for file names
val dateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)

// Create use cases
var videoCapture: VideoCapture<Recorder>? by remember { mutableStateOf(null) }
val preview = remember { Preview.Builder().build() }

// Create preview view
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}

val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()

// Set up the preview use case
preview.setSurfaceProvider(previewView.surfaceProvider)

// Set up video capture use case
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(
Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)
))
.build()

videoCapture = VideoCapture.withOutput(recorder)

try {
// Unbind all use cases and rebind with new ones
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture
)
} catch (e: Exception) {
Log.e("VideoRecording", "Use case binding failed", e)
onError("Camera initialization failed: ${e.message}")
}
}, ContextCompat.getMainExecutor(ctx))

previewView
},
modifier = Modifier.fillMaxSize()
)

// Record button
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 32.dp),
contentAlignment = Alignment.BottomCenter
) {
FloatingActionButton(
onClick = {
if (isRecording) {
// Stop recording
recording?.stop()
isRecording = false
} else {
// Start recording
val videoCapture = videoCapture ?: return@FloatingActionButton

// Create output file
val name = "VIDEO_${dateFormat.format(Date())}"

val contentValues = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, name)
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}

val mediaStoreOutput = MediaStoreOutputOptions.Builder(
context.contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
)
.setContentValues(contentValues)
.build()

// Configure options
recording = videoCapture.output
.prepareRecording(context, mediaStoreOutput)
.apply {
if (PermissionChecker.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PermissionChecker.PERMISSION_GRANTED
) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { event ->
when(event) {
is VideoRecordEvent.Start -> {
isRecording = true
}
is VideoRecordEvent.Finalize -> {
isRecording = false
if (event.hasError()) {
recording = null
onError("Video recording failed: ${event.error}")
} else {
event.outputResults.outputUri?.let { uri ->
onVideoSaved(uri)
}
}
}
}
}
}
},
containerColor = if (isRecording) Color.Red else MaterialTheme.colorScheme.primary,
modifier = Modifier.size(72.dp)
) {
Icon(
if (isRecording) Icons.Default.Stop else Icons.Default.Videocam,
contentDescription = if (isRecording) "Stop recording" else "Start recording",
modifier = Modifier.size(36.dp)
)
}
}
}
}

🧠 Adding Machine Learning with CameraX and ML Kit

One of the most powerful features of CameraX is its seamless integration with ML Kit. This allows you to add intelligent features like barcode scanning, face detection, text recognition, and more to your camera application.

Implementing Barcode Scanning

Here's how to implement a real-time QR code and barcode scanner in your app:

import androidx.camera.core.*
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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 androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode

@Composable
fun BarcodeScanner(
onBarcodeDetected: (String) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

// State for detected barcode value
var barcodeValue by remember { mutableStateOf<String?>(null) }

// Create barcode scanner
val barcodeScanner = remember {
BarcodeScanning.getClient()
}

// Use CameraController from CameraX
val cameraController = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.IMAGE_ANALYSIS)

setImageAnalysisAnalyzer(
ContextCompat.getMainExecutor(context),
MlKitAnalyzer(
listOf(barcodeScanner),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
ContextCompat.getMainExecutor(context)
) { analyzedImage ->
// Process detection results
val barcodeResults = analyzedImage.getResults(barcodeScanner)

if (barcodeResults != null && barcodeResults.size > 0) {
// Get first valid barcode
for (barcode in barcodeResults) {
// We're interested in display-value, which shows the decoded text
barcode.displayValue?.let { value ->
if (value != barcodeValue) {
barcodeValue = value
onBarcodeDetected(value)
}
}
}
}
}
)
}
}

Box(modifier = Modifier.fillMaxSize()) {
// Camera preview
AndroidView(
factory = {
androidx.camera.view.PreviewView(it).apply {
controller = cameraController
implementationMode = androidx.camera.view.PreviewView.ImplementationMode.PERFORMANCE
lifecycleOwner.lifecycle.addObserver(cameraController)
}
},
modifier = Modifier.fillMaxSize()
)

// Scanner overlay
Box(
modifier = Modifier
.size(300.dp)
.align(Alignment.Center)
.border(2.dp, Color.White)
)

// Display detected barcode value
barcodeValue?.let { value ->
Card(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Detected Barcode:",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = value,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
}

Adding Other ML Kit Features

You can easily extend this approach to implement other ML Kit features:

  1. Face Detection: Great for selfie apps or augmented reality effects
  2. Text Recognition: Perfect for document scanning apps
  3. Object Detection: Identify objects in the camera view
  4. Pose Detection: Track body positions for fitness apps

For example, here's how to implement face detection:

import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions

// Create a face detector with desired options
val faceDetector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.build()
)

// Use the face detector with MlKitAnalyzer
cameraController.setImageAnalysisAnalyzer(
ContextCompat.getMainExecutor(context),
MlKitAnalyzer(
listOf(faceDetector),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
ContextCompat.getMainExecutor(context)
) { analyzedImage ->
val faceResults = analyzedImage.getResults(faceDetector)
// Process detected faces
}
)

✨ Adding Professional Photography Features with Camera Extensions

CameraX Extensions allow your app to access advanced camera features provided by device manufacturers, such as HDR (High Dynamic Range), Night mode, Portrait/Bokeh effect, and more. These extensions enable your app to take professional-quality photos without needing to implement complex camera algorithms yourself.

Available Camera Extensions

ExtensionDescriptionCommon Use Cases
HDRHigh Dynamic RangeScenes with bright and dark areas, landscapes, backlit subjects
NightLow-light enhancementIndoor photos, evening/night photography
BokehPortrait mode with background blurPortraits, product photography
AutoDevice-optimized automatic modeGeneral photography
Face RetouchSkin smoothing and enhancementSelfies, portraits
BeautyAdvanced face enhancementSocial media photos, selfies

Implementing Camera Extensions

import androidx.camera.core.*
import androidx.camera.extensions.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 androidx.core.content.ContextCompat
import kotlinx.coroutines.launch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

@Composable
fun ExtendedCameraScreen(
onImageCaptured: (Uri) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()

// State for current extension mode
var currentExtensionMode by remember { mutableStateOf(ExtensionMode.NONE) }

// Camera provider and extensions manager
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraProvider = remember { mutableStateOf<ProcessCameraProvider?>(null) }
val extensionsManager = remember { mutableStateOf<ExtensionsManager?>(null) }

// Image capture use case
val imageCapture = remember {
ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.build()
}

// Initialize camera provider and extensions manager
LaunchedEffect(Unit) {
scope.launch {
try {
cameraProvider.value = cameraProviderFuture.get()
extensionsManager.value = ExtensionsManager.getInstanceAsync(
context,
cameraProvider.value!!
).get()
} catch (e: Exception) {
// Handle initialization errors
}
}
}

// Function to bind camera with selected extension
fun bindCameraWithExtension(extensionMode: Int, previewView: PreviewView) {
val cameraProvider = cameraProvider.value ?: return
val extManager = extensionsManager.value ?: return

try {
// Check if extension is available
if (!extManager.isExtensionAvailable(
CameraSelector.DEFAULT_BACK_CAMERA,
extensionMode
)
) {
// Extension not available, use default mode
bindCameraWithExtension(ExtensionMode.NONE, previewView)
return
}

// Get extension-enabled camera selector
val cameraSelector = extManager.getExtensionEnabledCameraSelector(
CameraSelector.DEFAULT_BACK_CAMERA,
extensionMode
)

// Create preview use case
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}

// Unbind previous use cases
cameraProvider.unbindAll()

// Bind camera with extension
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)

currentExtensionMode = extensionMode
} catch (e: Exception) {
// Extension failed, fall back to default mode
if (extensionMode != ExtensionMode.NONE) {
bindCameraWithExtension(ExtensionMode.NONE, previewView)
}
}
}

// Camera UI
Box(modifier = Modifier.fillMaxSize()) {
// Preview
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}

cameraProviderFuture.addListener({
bindCameraWithExtension(currentExtensionMode, previewView)
}, ContextCompat.getMainExecutor(ctx))

previewView
},
modifier = Modifier.fillMaxSize(),
update = { previewView ->
// Update preview if extension mode changes
cameraProvider.value?.let {
bindCameraWithExtension(currentExtensionMode, previewView)
}
}
)

// Extension selector and capture button
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
// Extension mode selection
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
ExtensionButton("Auto", ExtensionMode.AUTO, currentExtensionMode) {
currentExtensionMode = ExtensionMode.AUTO
}

ExtensionButton("HDR", ExtensionMode.HDR, currentExtensionMode) {
currentExtensionMode = ExtensionMode.HDR
}

ExtensionButton("Night", ExtensionMode.NIGHT, currentExtensionMode) {
currentExtensionMode = ExtensionMode.NIGHT
}

ExtensionButton("Portrait", ExtensionMode.BOKEH, currentExtensionMode) {
currentExtensionMode = ExtensionMode.BOKEH
}

ExtensionButton("Normal", ExtensionMode.NONE, currentExtensionMode) {
currentExtensionMode = ExtensionMode.NONE
}
}

Spacer(modifier = Modifier.height(16.dp))

// Capture button
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
) {
FloatingActionButton(
onClick = {
// Take picture with current extension mode
val photoFile = File(
context.cacheDir,
"IMG_${System.currentTimeMillis()}.jpg"
)

val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.build()

imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
output.savedUri?.let { uri ->
onImageCaptured(uri)
} ?: onImageCaptured(Uri.fromFile(photoFile))
}

override fun onError(exception: ImageCaptureException) {
onError(exception)
}
}
)
},
modifier = Modifier.size(72.dp)
) {
Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = "Take photo",
modifier = Modifier.size(36.dp)
)
}
}

Spacer(modifier = Modifier.height(24.dp))
}
}
}

@Composable
fun ExtensionButton(
name: String,
mode: Int,
currentMode: Int,
onClick: () -> Unit
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = if (currentMode == mode)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(name)
}
}

Device Compatibility

Camera Extensions availability varies by device manufacturer. Your app should always check if an extension is available before attempting to use it, and provide a graceful fallback to standard camera mode when extensions are not supported.

To check extension availability programmatically:

val extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).get()

// Check if HDR is available on the back camera
val isHdrAvailable = extensionsManager.isExtensionAvailable(
CameraSelector.DEFAULT_BACK_CAMERA,
ExtensionMode.HDR
)

🛠 Best Practices: Permission Handling and Error Management

Implementing Runtime Permissions

Modern Android apps require explicit permission handling. Here's how to properly request and manage camera permissions using the Accompanist Permissions library:

import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermissionsScreen(
onPermissionsGranted: @Composable () -> Unit
) {
// Request camera and microphone permissions
val permissionsState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)

val allPermissionsGranted = permissionsState.permissions.all {
it.status == PermissionStatus.Granted
}

if (allPermissionsGranted) {
// Permissions granted, show camera UI
onPermissionsGranted()
} else {
// Permission request UI
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val textToShow = if (permissionsState.shouldShowRationale) {
"Camera and microphone access is required to use the camera features of this app."
} else {
"Camera permission is required for this feature. Please grant the permission."
}

Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = null,
modifier = Modifier
.size(120.dp)
.padding(bottom = 16.dp)
)

Text(
text = textToShow,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)

Button(
onClick = { permissionsState.launchMultiplePermissionRequest() }
) {
Text("Request Permissions")
}
}
}
}

Error Handling Strategies

Robust error handling is crucial for camera applications. Here are the key errors to handle:

  1. Camera initialization failures

    • Device doesn't have a suitable camera
    • Camera is in use by another application
    • Insufficient system resources
  2. Image capture errors

    • Storage issues (no space, permissions)
    • Processing errors
  3. Video recording issues

    • Encoding failures
    • File size limitations
    • Recording interruptions

Example Error Handler Implementation

fun handleCameraError(error: Exception, context: Context): String {
return when (error) {
is CameraUnavailableException -> {
// Camera is currently unavailable
Log.e("CameraX", "Camera unavailable", error)
"Camera is unavailable. Another app might be using it."
}
is ImageCaptureException -> {
// Handle based on error code
when (error.imageCaptureError) {
ImageCapture.ERROR_CAPTURE_FAILED -> "Failed to capture image. Please try again."
ImageCapture.ERROR_FILE_IO -> "File error. Please check storage permissions."
ImageCapture.ERROR_CAMERA_CLOSED -> "Camera was closed during capture. Please try again."
else -> "Failed to save photo. Please try again."
}
}
else -> {
Log.e("CameraX", "Unexpected camera error", error)
"An unexpected error occurred. Please try again."
}
}
}

Performance Optimization Tips

  • Lifecycle Awareness: Always bind camera to the lifecycle to ensure proper resource cleanup
  • Resolution Management: Select appropriate resolution based on use case
  • Background Processing: Use WorkManager for processing captured media
  • Memory Management: Close image proxies after analysis
  • Threading: Use appropriate executors for camera operations

✅ Summary: Building Production-Ready Camera Apps

By following this comprehensive guide, you now have the knowledge to implement professional camera features in your Android app using CameraX and Jetpack Compose.

What You've Learned:

  • Camera Preview: Display real-time camera feed with Compose integration
  • Photo Capture: Take high-quality photos with various capture modes
  • Video Recording: Record videos with audio and quality options
  • ML Integration: Add intelligence to your camera with ML Kit
  • Advanced Photography: Leverage device-specific camera extensions like HDR and Night mode
  • Best Practices: Handle permissions, errors, and lifecycle events properly

Key Benefits of CameraX:

  • Cross-Device Compatibility: Works consistently on 90%+ of Android devices
  • Modern Architecture: Clean, lifecycle-aware implementation
  • Simplified Development: Reduce camera code by up to 80% compared to Camera2 API
  • Future-Proof: Regular updates and improvements from Google

Next Steps:

Consider exploring these advanced CameraX topics:

  • Custom image processing pipelines
  • Concurrent use cases (e.g., analyzing while recording)
  • Camera extensions vendor integration
  • Testing camera implementations

For more information, check out the official CameraX documentation and CameraX release notes.


👨‍💻 About the Author

This tutorial was created by an experienced Android developer with expertise in camera implementations, ML Kit integration, and Jetpack Compose. If you found this guide helpful, please share it with other developers who might benefit from these camera integration techniques.


📚 Frequently Asked Questions (FAQs)

Q: Do I need to use Camera2 API alongside CameraX?

A: No, CameraX is built on top of Camera2 API, but abstracts away its complexity. You don't need to interact with Camera2 directly unless you have very specific requirements not covered by CameraX.

Q: Does CameraX work on all Android devices?

A: CameraX works on devices running Android 5.0 (API level 21) or higher, which covers approximately 98% of active Android devices. The camera extensions feature requires Android 7.0 (API level 24) or higher.

Q: How do I handle device orientation changes with CameraX?

A: CameraX handles orientation changes automatically in most cases. The captured images and videos will be correctly oriented based on the device orientation at capture time.

Q: Can I use CameraX without Jetpack Compose?

A: Absolutely! CameraX works with both traditional XML-based layouts and Jetpack Compose. This tutorial focuses on Compose integration, but the core CameraX concepts apply to both UI approaches.

Q: How do I handle low memory situations during camera usage?

A: Implement the onLowMemory() callback in your activity or fragment and release non-essential camera resources. Also, consider reducing the resolution of preview or capture if memory is constrained.