Skip to main content

Jetpack Compose Integration

This guide covers integrating the Paper Receipt camera UI into a Jetpack Compose–based Android application. It describes two primary integration strategies: using RecognizerView to build a custom scanning UI, and using the out-of-the-box camera experience for a complete, pre-built camera flow.

Custom Scan using RecognizerView

To integrate RecognizerView with Jetpack Compose, use the AndroidView composable to embed the traditional Android View inside your Compose UI. Manage the view's lifecycle by creating it in the factory lambda and releasing it in onRelease.

The following example sets up RecognizerView inside a composable:

@Composable
fun RecognizerViewComposable(
modifier: Modifier,
// ...
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

// Use remember to prevent the RecognizerView from being re-created on every recomposition.
val recognizerView = remember {
RecognizerView(context).apply {
// Observe Recognizer Callback
recognizerCallback(object: CameraRecognizerCallback {
// ...
})

// Initialize with ScanOptions
initialize(
ScanOptions
.newBuilder()
// ...
.build()
)

// Attach Lifecycle Owner
lifecycle(lifecycleOwner)
}
}

AndroidView(
factory = { recognizerView },
modifier = modifier,
onRelease = { view ->
view.terminate()
}
)
}

Out-of-the-box Camera Experience

There are two ways to integrate the pre-built camera experience: embedding CameraRecognizerFragment directly in a composable for a tightly integrated UI, or launching it as a separate activity through a CameraRecognizerContract for a more decoupled flow.

1. Embed CameraRecognizerFragment using AndroidFragment

This approach embeds CameraRecognizerFragment within a composable using the AndroidFragment composable from the androidx.fragment:fragment-compose library. It's a good fit if your app already uses a Fragment-based architecture, or if you want to place the camera view as a component within a larger composable screen. The camera's lifecycle and results are managed directly within the composable's context.

// build.gradle
implementation "androidx.fragment:fragment-compose:1.8.9"
import androidx.fragment.app.FragmentActivity

class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CameraRecognizerContent(
modifier = Modifier,
onScanResults = { results: CameraRecognizerResults ->
// Process CameraRecognizerResults here...
}
)
}
}
}
@Composable
internal fun CameraRecognizerContent(
modifier: Modifier,
onScanResults: ((CameraRecognizerResults) -> Unit),
) {
val lifecycleOwner = LocalLifecycleOwner.current

AndroidFragment<CameraRecognizerFragment>(
modifier = modifier.fillMaxSize(),
arguments = bundleOf(
CameraRecognizerFragment.OPTIONS to ScanOptions
.newBuilder()
// Define your own ScanOptions configuration here...
.build(),
CameraRecognizerFragment.CAMERA_CHARACTERISTICS to CameraCharacteristics.Builder()
.scanCharacteristics(
ScanCharacteristics.Builder()
// Define your own ScanCharacteristics configuration here...
.build()
)
.tooltipCharacteristics(
TooltipCharacteristics.Builder()
// Define your own TooltipCharacteristics configuration here...
.build()
)
// Define other CameraCharacteristics configuration here...
.build(),
),
) { fragment ->
fragment.parentFragmentManager
.setFragmentResultListener(
CameraRecognizerFragment.SCAN_SESSION_RESULTS_KEY,
lifecycleOwner,
) { _, bundle ->
bundle.parcelable<CameraRecognizerResults>(CameraRecognizerFragment.SCAN_RESULTS_KEY)
?.let { results ->
// Retrieve CameraRecognizerResults here...
onScanResults(results)
}
}
}
}

2. Launch as an Activity using CameraRecognizerContract

This approach leverages the modern Android Activity Result APIs. By pairing rememberLauncherForActivityResult with CameraRecognizerContract, you launch the camera recognizer as a separate activity and receive the results in a callback. It's ideal for a more decoupled architecture where scanning is a distinct step in a user flow, and it simplifies state management because the camera UI is entirely separate from your calling composable.

@Composable
internal fun CameraRecognizerContent(
modifier: Modifier,
onScanResults: ((CameraRecognizerResults) -> Unit),
) {
val launcher = rememberLauncherForActivityResult(
CameraRecognizerContract()
) { results: CameraRecognizerResults ->
onScanResults(results)
}

// Launch the camera recognizer when this composable enters the composition
LaunchedEffect(Unit) {
launcher.launch(
CameraRecognizerOptions.Builder()
.options(
ScanOptions
.newBuilder()
// Define your own ScanOptions configuration here...
.build()
)
.characteristics(
CameraCharacteristics.Builder()
// Define other CameraCharacteristics configuration here...
.scanCharacteristics(
ScanCharacteristics.Builder()
// Define your own ScanCharacteristics configuration here...
.build()
)
.tooltipCharacteristics(
TooltipCharacteristics.Builder()
// Define your own TooltipCharacteristics configuration here...
.build()
)
.build()
)
.build()
)
}
}