0Pricing
Android Academy · Lesson

Showing a Camera Preview

Wire up a live preview.

Showing a Camera Preview is a free Android Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Goal: A Live Preview

The first thing almost every camera app needs is a live preview — a real-time view of what the camera sees. In this lesson you'll build a Compose screen that shows the camera feed full-screen.

You'll combine three pieces: a PreviewView, the Preview use case and a bound ProcessCameraProvider.

Requesting the Camera Permission

Before you can show a preview you need the user's permission. In Compose, Accompanist (or the official permissions API) makes this easy with rememberPermissionState.

Only start the camera once the permission is granted.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraScreen() {
    val permission = rememberPermissionState(Manifest.permission.CAMERA)

    LaunchedEffect(Unit) { permission.launchPermissionRequest() }

    if (permission.status.isGranted) {
        CameraPreview()
    } else {
        Text("Camera permission is required.")
    }
}

Embedding PreviewView in Compose

PreviewView is a classic Android View, so you host it inside Compose with AndroidView. The factory lambda creates the view once and gives you a reference you can configure.

Store the created view so you can attach the camera surface to it.

@Composable
fun CameraPreview(modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            PreviewView(context).apply {
                scaleType = PreviewView.ScaleType.FILL_CENTER
            }
        }
    )
}

Getting the LifecycleOwner

To bind the camera you need a LifecycleOwner. In Compose you read the current one with LocalLifecycleOwner.current, and the context with LocalContext.current.

Both are needed to set up the camera provider and bind use cases.

val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

Building the Preview Use Case

The Preview use case streams frames to a surface. After building it, you connect it to your PreviewView by calling setSurfaceProvider with the view's surface provider.

This is the link that makes the camera image actually appear on screen.

val preview = Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)

Binding Inside a Coroutine

Getting the provider is asynchronous, so do it in a coroutine. Use LaunchedEffect tied to the view so it runs once. Inside, await the provider, unbind anything previous, then bind the preview.

LaunchedEffect(previewView) {
    val cameraProvider = ProcessCameraProvider.getInstance(context).await()
    val preview = Preview.Builder().build().also {
        it.setSurfaceProvider(previewView.surfaceProvider)
    }
    cameraProvider.unbindAll()
    cameraProvider.bindToLifecycle(
        lifecycleOwner,
        CameraSelector.DEFAULT_BACK_CAMERA,
        preview
    )
}

The await() Extension

ProcessCameraProvider.getInstance() returns a ListenableFuture. The await() extension from kotlinx-coroutines-guava suspends until it resolves, turning callback code into clean sequential code.

Add the dependency org.jetbrains.kotlinx:kotlinx-coroutines-guava to use it.

import kotlinx.coroutines.guava.await

// Without await you'd use a listener:
// future.addListener({ val provider = future.get() }, executor)
// await() makes it a single suspend call instead.

Putting It Together

Here is the complete preview composable. Notice the flow: create the PreviewView in factory, capture it, then bind inside LaunchedEffect.

@Composable
fun CameraPreview(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val previewView = remember { PreviewView(context) }

    LaunchedEffect(previewView) {
        val provider = ProcessCameraProvider.getInstance(context).await()
        val preview = Preview.Builder().build().also {
            it.setSurfaceProvider(previewView.surfaceProvider)
        }
        provider.unbindAll()
        provider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview
        )
    }

    AndroidView(
        factory = { previewView },
        modifier = modifier.fillMaxSize()
    )
}

Scale Types

The camera sensor's aspect ratio rarely matches your screen. PreviewView.ScaleType controls how the feed fits the view:

  • FILL_CENTER — fill the view, cropping edges (most common).
  • FIT_CENTER — show the whole frame with letterboxing.

Pick FILL_CENTER for an immersive full-screen camera.

previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
// or
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER

Switching Cameras

To toggle between front and back, store the selector in state and re-bind when it changes. Because LaunchedEffect re-runs on a key change, you just include the selector as a key.

var selector by remember { mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) }

LaunchedEffect(selector) {
    val provider = ProcessCameraProvider.getInstance(context).await()
    provider.unbindAll()
    provider.bindToLifecycle(lifecycleOwner, selector, preview)
}

// Flip button toggles 'selector' between front and back.

Common Pitfalls

Two mistakes cause a black preview:

  • Forgetting setSurfaceProvider — frames have nowhere to go.
  • Not calling unbindAll() before re-binding — CameraX throws if a use case is bound twice.

Also remember: the preview won't start until the permission is actually granted.

Quick Check

Check your understanding of wiring up the preview.

Recap

You built a live camera preview in Compose:

  • Request the CAMERA permission before starting.
  • Host a PreviewView with AndroidView.
  • Await the ProcessCameraProvider in a coroutine.
  • Build a Preview, call setSurfaceProvider, unbindAll(), then bindToLifecycle.

Next, you'll add the ImageCapture use case and take a real photo.

Frequently asked questions

Is the “Showing a Camera Preview” lesson free?

Yes — the full text of “Showing a Camera Preview” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Showing a Camera Preview”?

Wire up a live preview. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Showing a Camera Preview” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. CameraX Overview
  2. Showing a Camera Preview
  3. Taking Photos
  4. Recording Video
← Back to Android Academy