CameraX
So far TrailLog has treated a trail photo as something that already exists on disk. This lesson covers taking that photo inside TrailLog itself, using CameraX — Jetpack's modern camera library.
Why CameraX Instead of Camera2 Directly
The older Camera2 API gives an app direct, low-level control over device camera
hardware — and that's exactly its problem for most apps: it's notoriously inconsistent across
manufacturers, requiring device-specific workarounds for things as basic as image rotation and
aspect ratio that should just work the same everywhere. CameraX is built on top of
Camera2 but deliberately designed to smooth all of that over, giving you one consistent API that
behaves the same across the huge range of Android devices and camera hardware in the real world.
For the overwhelming majority of apps — TrailLog included — CameraX is the right choice.
CameraX's Core Use Cases
| Use case | What it's for |
|---|---|
Preview | Shows a live camera feed on screen, so the user can frame their shot before capturing. |
ImageCapture | Takes a single still photo and saves it to a file — this is the one TrailLog actually needs. |
ImageAnalysis | Streams live camera frames to your own code for real-time processing (e.g. detecting a barcode). TrailLog doesn't need this, but it's worth knowing CameraX supports it for apps that do. |
Step 1: A PreviewView in TrailLog's "Add Photo" Screen
The layout for the screen where a user photographs a new trail needs a PreviewView —
CameraX's dedicated view for rendering the live camera feed:
<androidx.camera.view.PreviewView
android:id="@+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/captureButton" />
<Button
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Capture" />
Step 2: Binding Use Cases to the Lifecycle
With the CAMERA runtime permission already granted (following the same pattern as
Runtime Permissions), CameraX is wired up by fetching
a ProcessCameraProvider and binding the use cases you need to an Activity or Fragment's
lifecycle:
private lateinit var imageCapture: ImageCapture
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageCapture
)
}, ContextCompat.getMainExecutor(this))
}
bindToLifecycle(this, ...) is the part that matters most here: it hands CameraX the
Activity itself as a LifecycleOwner, so CameraX automatically starts the camera when
the screen becomes visible and stops it when the screen is paused or destroyed. Without lifecycle
binding, it's easy to accidentally leave the camera hardware open after the user has already
navigated away — a real resource leak, and one CameraX's design specifically avoids by tying camera
usage directly to the screen's own lifecycle.
Step 3: Capturing a Photo and Saving It via FileProvider
ImageCapture.takePicture() writes the photo directly to a destination file you provide,
using the same app-specific external "Pictures" folder and FileProvider setup from
FileProvider & Content Providers:
private fun captureTrailPhoto(trailId: Int) {
val photoFile = File(getExternalFilesDir("Pictures"), "trail_${trailId}.jpg")
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val photoUri = FileProvider.getUriForFile(
this@TrailPhotoActivity,
"com.codingwithsonu.traillog.fileprovider",
photoFile
)
// save photoUri onto this trail's record
}
override fun onError(exception: ImageCaptureException) {
// show an error — capture failed
}
}
)
}
Note the same FileProvider.getUriForFile() call from the earlier lesson reappears here
unchanged — capturing a photo with CameraX and sharing an existing photo through a share sheet both
end up needing the exact same safe, scoped content:// URI.
The CAMERA Runtime Permission
Like fine location or notifications, camera access requires the CAMERA runtime
permission, requested with the same ActivityResultContracts.RequestPermission()
pattern covered in Runtime Permissions — request it
right before calling startCamera(), never assume it's already granted.
One Less Headache: Rotation Handled for You
One genuinely painful problem with hand-rolled camera code used to be getting photo rotation right across different devices and orientations — photos coming out sideways depending on how the phone was held. CameraX handles sensor rotation and lens orientation correctly out of the box, so TrailLog's captured photos display right-side-up without any manual rotation math.