Android CameraX in 2026: Photo and Video Capture with Interview Questions

Master Android CameraX 1.6 with practical photo and video capture implementation. Covers Preview, ImageCapture, VideoCapture use cases, CameraPipe architecture, and common interview questions.

Android CameraX camera app development tutorial for photo and video capture

Android CameraX simplifies camera development by abstracting hardware complexity behind a lifecycle-aware API. With version 1.6.2 now stable and the migration to CameraPipe complete, CameraX delivers the same high-performance camera stack that powers the Pixel camera app.

CameraX 1.6 Key Changes

CameraX 1.6 migrated to CameraPipe, uses Media3 Muxer by default for VideoCapture, and stabilized the SessionConfig API. The Kotlin DSL builders (preview { }, imageCapture { }, videoCapture { }) reduce boilerplate by 40%.

CameraX Architecture and Use Case Model

CameraX provides four primary use cases that can run concurrently: Preview, ImageCapture, ImageAnalysis, and VideoCapture. Each use case binds to a LifecycleOwner, so the camera automatically starts and stops with the activity or fragment.

The CameraProvider acts as a factory that instantiates these use cases and binds them to the camera. This decoupling means the same code runs on devices from different OEMs without worrying about vendor-specific quirks.

CameraSetup.ktkotlin
class CameraSetup(
    private val context: Context,
    private val lifecycleOwner: LifecycleOwner
) {
    private lateinit var cameraProvider: ProcessCameraProvider
    private var imageCapture: ImageCapture? = null
    private var videoCapture: VideoCapture<Recorder>? = null
    
    suspend fun initialize() {
        // ProcessCameraProvider binds use cases to lifecycle
        cameraProvider = ProcessCameraProvider.getInstance(context).await()
    }
    
    fun bindUseCases(previewView: PreviewView) {
        // Unbind existing use cases before rebinding
        cameraProvider.unbindAll()
        
        // Preview use case displays camera feed
        val preview = Preview.Builder()
            .build()
            .apply { setSurfaceProvider(previewView.surfaceProvider) }
        
        // ImageCapture for photos
        imageCapture = ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
            .build()
        
        // VideoCapture with Recorder
        val recorder = Recorder.Builder()
            .setQualitySelector(QualitySelector.from(Quality.FHD))
            .build()
        videoCapture = VideoCapture.withOutput(recorder)
        
        // Bind all use cases to lifecycle
        cameraProvider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview,
            imageCapture,
            videoCapture
        )
    }
}

The bindToLifecycle call connects use cases to both the camera hardware and the lifecycle. When the LifecycleOwner enters STARTED, the camera opens. When it enters STOPPED, the camera closes automatically.

Implementing Photo Capture with ImageCapture

ImageCapture provides two capture modes: CAPTURE_MODE_MINIMIZE_LATENCY for fast shutter response, and CAPTURE_MODE_MAXIMIZE_QUALITY for processing-heavy shots. The choice depends on whether the app prioritizes speed (social apps) or quality (document scanners).

PhotoCaptureManager.ktkotlin
class PhotoCaptureManager(
    private val context: Context,
    private val imageCapture: ImageCapture
) {
    private val mainExecutor = ContextCompat.getMainExecutor(context)
    
    fun capturePhoto(onResult: (Uri?) -> Unit) {
        // Generate unique filename with timestamp
        val filename = "IMG_${System.currentTimeMillis()}.jpg"
        
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            // Store in Pictures/CameraX folder on Android 10+
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX")
            }
        }
        
        val outputOptions = ImageCapture.OutputFileOptions.Builder(
            context.contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ).build()
        
        imageCapture.takePicture(
            outputOptions,
            mainExecutor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(results: ImageCapture.OutputFileResults) {
                    // results.savedUri contains the MediaStore URI
                    onResult(results.savedUri)
                }
                
                override fun onError(exception: ImageCaptureException) {
                    Log.e("PhotoCapture", "Capture failed: ${exception.message}")
                    onResult(null)
                }
            }
        )
    }
}

CameraX 1.5 added DNG (RAW) capture support for apps that need unprocessed sensor data. Check device capability with ImageCapture.getImageCaptureCapabilities() before enabling RAW output.

Recording Video with VideoCapture and Recorder

VideoCapture uses a Recorder object to handle encoding and muxing. The Recorder API follows a prepare-start-stop pattern that handles audio synchronization automatically.

VideoRecordingManager.ktkotlin
class VideoRecordingManager(
    private val context: Context,
    private val videoCapture: VideoCapture<Recorder>
) {
    private var activeRecording: Recording? = null
    
    fun startRecording(onEvent: (VideoRecordEvent) -> Unit): Boolean {
        // Check if already recording
        if (activeRecording != null) return false
        
        val filename = "VID_${System.currentTimeMillis()}.mp4"
        
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX")
            }
        }
        
        val mediaStoreOutput = MediaStoreOutputOptions.Builder(
            context.contentResolver,
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI
        )
            .setContentValues(contentValues)
            .build()
        
        // Prepare recording with audio if permission granted
        val pendingRecording = videoCapture.output
            .prepareRecording(context, mediaStoreOutput)
        
        if (hasAudioPermission()) {
            pendingRecording.withAudioEnabled()
        }
        
        // Start recording and store reference
        activeRecording = pendingRecording.start(
            ContextCompat.getMainExecutor(context)
        ) { event ->
            onEvent(event)
            
            // Clear reference when recording completes
            if (event is VideoRecordEvent.Finalize) {
                activeRecording = null
            }
        }
        
        return true
    }
    
    fun stopRecording() {
        activeRecording?.stop()
    }
    
    fun pauseRecording() {
        activeRecording?.pause()
    }
    
    fun resumeRecording() {
        activeRecording?.resume()
    }
    
    private fun hasAudioPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }
}

CameraX 1.5 added slow-motion video support. Query capabilities with Recorder.getHighSpeedVideoCapabilities() before enabling 120fps or 240fps recording.

Ready to ace your Android interviews?

Practice with our interactive simulators, flashcards, and technical tests.

CameraX with Jetpack Compose

CameraX 1.5 introduced the camera-compose artifact with a CameraXViewfinder composable. For apps still using View-based PreviewView, AndroidView provides interop.

CameraPreviewComposable.ktkotlin
@Composable
fun CameraPreview(
    modifier: Modifier = Modifier,
    onPreviewViewCreated: (PreviewView) -> Unit
) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            PreviewView(context).apply {
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                scaleType = PreviewView.ScaleType.FILL_CENTER
                onPreviewViewCreated(this)
            }
        }
    )
}

@Composable
fun CameraScreen(viewModel: CameraViewModel = viewModel()) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    LaunchedEffect(Unit) {
        viewModel.initializeCamera(context, lifecycleOwner)
    }
    
    Box(modifier = Modifier.fillMaxSize()) {
        CameraPreview(
            onPreviewViewCreated = { previewView ->
                viewModel.bindPreview(previewView)
            }
        )
        
        // Capture buttons overlay
        Row(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(32.dp),
            horizontalArrangement = Arrangement.spacedBy(24.dp)
        ) {
            IconButton(
                onClick = { viewModel.capturePhoto() }
            ) {
                Icon(Icons.Default.Camera, "Take photo")
            }
            
            IconButton(
                onClick = { viewModel.toggleRecording() }
            ) {
                Icon(
                    if (viewModel.isRecording) Icons.Default.Stop
                    else Icons.Default.Videocam,
                    "Record video"
                )
            }
        }
    }
}

For Compose-first implementations, the Jetpack Compose interview questions guide covers ViewModel integration patterns that apply directly to camera state management.

Handling Camera Permissions

Android requires CAMERA permission at runtime. RECORD_AUDIO is needed only when recording video with sound. The permission flow should explain why access is needed before requesting.

PermissionHandler.ktkotlin
class CameraPermissionHandler(private val activity: ComponentActivity) {
    private val requiredPermissions = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )
    
    private val permissionLauncher = activity.registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val cameraGranted = permissions[Manifest.permission.CAMERA] == true
        val audioGranted = permissions[Manifest.permission.RECORD_AUDIO] == true
        
        if (cameraGranted) {
            onCameraPermissionGranted(audioGranted)
        } else {
            onCameraPermissionDenied()
        }
    }
    
    fun checkAndRequestPermissions() {
        when {
            hasAllPermissions() -> onCameraPermissionGranted(hasAudioPermission())
            shouldShowRationale() -> showPermissionRationale()
            else -> permissionLauncher.launch(requiredPermissions)
        }
    }
    
    private fun hasAllPermissions(): Boolean {
        return requiredPermissions.all { permission ->
            ContextCompat.checkSelfPermission(
                activity, permission
            ) == PackageManager.PERMISSION_GRANTED
        }
    }
    
    private fun hasAudioPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            activity,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun shouldShowRationale(): Boolean {
        return requiredPermissions.any { permission ->
            ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
        }
    }
}

CameraX Interview Questions

CameraX appears in Android interviews when discussing camera integration, lifecycle management, or hardware abstraction. These questions test understanding of the architecture and practical implementation details.

Why use CameraX instead of Camera2?

CameraX abstracts device-specific quirks that Camera2 exposes directly. A Camera2 implementation requires handling hundreds of device-specific workarounds. CameraX bakes these fixes into the library, tested against the CameraX Test Lab that covers 150+ device models.

The lifecycle-aware binding eliminates manual resource management. Camera2 requires explicit open/close calls that cause leaks when not handled correctly in every lifecycle callback.

How does CameraProvider bind use cases to lifecycle?

CameraProvider uses the LifecycleObserver pattern internally. When calling bindToLifecycle(), it registers an observer on the provided LifecycleOwner. This observer receives ON_START and ON_STOP events to open and close the camera.

The key insight: CameraX does not start the camera immediately on bind. It waits for the lifecycle to reach STARTED state, which matches Activity.onStart() timing.

What happens when you bind incompatible use cases?

Not all use case combinations work on all devices. Preview + VideoCapture + ImageAnalysis + ImageCapture may fail on older hardware. CameraX throws an IllegalArgumentException at bind time, not at runtime.

The solution is checking CameraProvider.hasCamera() and device capabilities before binding complex combinations. For critical apps, test on low-end devices explicitly.

Explain the difference between CAPTURE_MODE_MINIMIZE_LATENCY and CAPTURE_MODE_MAXIMIZE_QUALITY

MINIMIZE_LATENCY reduces shutter lag by using faster processing pipelines, possibly skipping some post-processing. MAXIMIZE_QUALITY applies full HDR+ processing, noise reduction, and multi-frame capture on supported devices.

The tradeoff: latency mode captures in 100-200ms, quality mode may take 500ms-1s depending on lighting. Social apps typically choose latency. Document scanning apps choose quality.

How does CameraX handle device rotation?

CameraX automatically handles rotation through the target rotation setting. By default, it uses the Display rotation, but apps can override with setTargetRotation(). The output image EXIF data contains the correct orientation tag.

For PreviewView, the scaleType handles aspect ratio differences. FILL_CENTER crops to fill, FIT_CENTER shows letterboxing. The choice depends on whether the app prioritizes full-screen preview or avoiding cropping.

Dependencies and Setup for CameraX 1.6

The CameraX library is split into multiple artifacts. Include only what the app uses to minimize APK size.

build.gradle.kts (Module: app)kotlin
dependencies {
    val cameraxVersion = "1.6.2"
    
    // Core library required for all use cases
    implementation("androidx.camera:camera-core:$cameraxVersion")
    
    // Camera2 implementation (required)
    implementation("androidx.camera:camera-camera2:$cameraxVersion")
    
    // Lifecycle integration
    implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
    
    // VideoCapture use case
    implementation("androidx.camera:camera-video:$cameraxVersion")
    
    // PreviewView and camera UI components
    implementation("androidx.camera:camera-view:$cameraxVersion")
    
    // Optional: Extensions (Night mode, HDR, etc.)
    implementation("androidx.camera:camera-extensions:$cameraxVersion")
    
    // Optional: Real-time effects
    implementation("androidx.camera:camera-effects:$cameraxVersion")
}

android {
    // CameraX requires Java 11 bytecode
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

The Manifest needs feature declarations and permissions:

xml
<!-- AndroidManifest.xml -->
<manifest>
    <!-- Camera hardware requirement -->
    <uses-feature android:name="android.hardware.camera.any" />
    
    <!-- Runtime permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>

Related patterns for Kotlin Coroutines apply when wrapping CameraX ListenableFuture calls in suspend functions.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

What CameraX 1.6 Changes for Production Apps

CameraX 1.6 brought three changes that affect existing implementations:

  1. CameraPipe backend: The internal camera stack now uses CameraPipe, the same architecture as the Pixel camera. This improves performance on Pixel devices and provides a cleaner foundation for advanced features. Existing code does not need changes.

  2. Media3 Muxer default: VideoCapture now uses Media3 Muxer instead of MediaMuxer. This improves crash resilience: if the app terminates during recording, the video file remains playable. The API surface is unchanged.

  3. SessionConfig API stable: Apps can now configure advanced camera settings through SessionConfig without experimental annotations. This enables features like simultaneous front/back camera use on supported devices.

Migration path: Update the dependency version. Test on devices that previously had issues. The CameraX release notes document any behavioral changes between versions.

  • CameraX uses lifecycle-aware binding: bindToLifecycle() connects use cases to both camera and Activity lifecycle
  • ImageCapture modes trade latency for quality: choose based on use case, not device capability
  • VideoCapture requires Recorder setup before binding: prepare the output options before starting
  • Permission handling should explain purpose before requesting: reduces rejection rates
  • CameraX 1.6 migrated to CameraPipe internally: no API changes required, but test on target devices
  • Interview questions focus on lifecycle management, use case combinations, and Camera2 comparison
Daily challenge

Can you spot the bug in Android?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 11, 2026

Tags

#android
#camerax
#jetpack
#kotlin
#camera
#video

Share

Related articles