Setting Up Camera Integration
In this segment, you’ll add camera functionality to your Task Manager app. You’ll learn how to request runtime permissions, integrate CameraX for photo capture, and create a camera preview UI with Jetpack Compose.
Camera access requires handling Android platform APIs and runtime permissions, which you’ll implement in the Kotlin layer. The captured photos will later be processed in Swift, demonstrating how platform integration and business logic can live in different layers.
Getting Started
Access the starter project from the Materials section in the right sidebar. Open the Starter project in Android Studio and let it sync. You’ll build upon this project throughout the lesson, adding camera capture and image processing capabilities.
Understanding Android Camera Permissions
Android requires explicit user permission to access the camera. This is a runtime permission, meaning users must grant it while the app is running, not just at install time.
You handle permissions in the Android layer because:
- Permissions are part of the Android platform API.
- The permission dialog is controlled by the Android system.
- Permission state is managed by Android’s PackageManager.
- Compose provides built-in permission handling utilities.
Swift code will receive the photo data after permission is granted and the photo is captured, keeping platform concerns separate from business logic.
Adding Camera Dependencies
First, add the necessary dependencies to your project.
Open app/build.gradle.kts and add these dependencies inside the dependencies block, after the Compose dependencies:
dependencies {
// Existing dependencies...
// CameraX dependencies
implementation("androidx.camera:camera-core:1.4.1")
implementation("androidx.camera:camera-camera2:1.4.1")
implementation("androidx.camera:camera-lifecycle:1.4.1")
implementation("androidx.camera:camera-view:1.4.1")
// Image loading library
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("androidx.exifinterface:exifinterface:1.3.7")
// Permission handling
implementation("com.google.accompanist:accompanist-permissions:0.36.0")
}
Sync your project with Gradle after adding these dependencies.
What each dependency does:
- camera-core: Core CameraX functionality.
- camera-camera2: Camera2 API implementation.
- camera-lifecycle: Lifecycle-aware camera management.
- camera-view: UI components for camera preview.
- coil-compose: Async image loading for Compose.
- exifinterface: Handles image rotation metadata from camera captures.
- accompanist-permissions: Permission handling utilities for Compose.
Configuring the Android Manifest
Next, declare the camera permission in your app’s manifest.
Open app/src/main/AndroidManifest.xml and add these lines before <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application ...>
<!-- Rest of your manifest -->
</application>
</manifest>
What these declarations do:
- uses-permission: Requests camera access permission from the user.
- uses-feature: Declares that your app can use the camera, but it isn’t required (android:required=“false” allows installation on devices without cameras).
Creating the Permission Handler
Now create a composable to handle camera permission requests.
Create a new file app/src/main/java/com/kodeco/android/swiftsdkforandroid/taskmanager/ui/PermissionHandler.kt:
package com.kodeco.android.swiftsdkforandroid.taskmanager.ui
import android.Manifest
import androidx.compose.runtime.*
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
// 1
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermissionHandler(
onPermissionGranted: @Composable () -> Unit,
onPermissionDenied: @Composable () -> Unit
) {
// 2
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
// 3
LaunchedEffect(Unit) {
if (!cameraPermissionState.status.isGranted) {
cameraPermissionState.launchPermissionRequest()
}
}
// 4
when {
cameraPermissionState.status.isGranted -> {
onPermissionGranted()
}
else -> {
onPermissionDenied()
}
}
}
What each section does:
- OptIn annotation: Uses experimental Accompanist permissions API.
- rememberPermissionState: Creates permission state for the camera.
- LaunchedEffect: Automatically requests permission if not granted.
- when statement: Renders different content based on permission status.
This composable checks if the camera permission is granted. If not, it shows the system permission dialog. Based on the user’s response, it calls either onPermissionGranted or onPermissionDenied.
Creating the Camera Screen
Next, create the camera preview and capture UI.
Create a new file app/src/main/java/com/kodeco/android/swiftsdkforandroid/taskmanager/ui/CameraScreen.kt:
package com.kodeco.android.swiftsdkforandroid.taskmanager.ui
import android.content.Context
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
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.Close
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.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
// 1
@Composable
fun CameraScreen(
onPhotoCaptured: (Uri) -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
// 2
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
var imageCapture: ImageCapture? by remember { mutableStateOf(null) }
Box(modifier = Modifier.fillMaxSize()) {
// 3
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val imageCaptureBuilder = ImageCapture.Builder()
imageCapture = imageCaptureBuilder.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
} catch (e: Exception) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(ctx))
previewView
},
modifier = Modifier.fillMaxSize()
)
// 4
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
// 5
IconButton(
onClick = onDismiss,
modifier = Modifier.align(Alignment.End)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close camera",
tint = MaterialTheme.colorScheme.onPrimary
)
}
// 6
FloatingActionButton(
onClick = {
capturePhoto(context, imageCapture) { uri ->
onPhotoCaptured(uri)
}
},
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = "Capture photo"
)
}
}
}
}
// 7
private fun capturePhoto(
context: Context,
imageCapture: ImageCapture?,
onPhotoCaptured: (Uri) -> Unit
) {
val photoFile = createPhotoFile(context)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture?.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
onPhotoCaptured(savedUri)
}
override fun onError(exception: ImageCaptureException) {
exception.printStackTrace()
}
}
)
}
// 8
private fun createPhotoFile(context: Context): File {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val photoFileName = "TASK_${timeStamp}.jpg"
val storageDir = context.getExternalFilesDir("photos")
if (storageDir?.exists() == false) {
storageDir.mkdirs()
}
return File(storageDir, photoFileName)
}
What each section does:
- CameraScreen composable: Main camera UI with preview and capture button.
- Camera setup: Gets CameraProvider and creates ImageCapture instance.
- AndroidView: Wraps Android’s PreviewView in Compose for camera preview.
- UI overlay: Adds close button and capture button over the preview.
- Close button: Dismisses the camera screen.
- Capture button: Triggers photo capture when tapped.
- capturePhoto function: Handles the actual photo capture process.
- createPhotoFile function: Creates a unique file for storing the captured photo.
This screen provides a full-screen camera preview with a floating action button to capture photos. When a photo is captured, it’s saved to the app’s external files directory and the Uri is returned through the onPhotoCaptured callback.
Build and run the app now (using ./gradlew assembleDebug or Android Studio’s Build menu) to ensure everything compiles correctly before moving forward. There won’t be any visual changes yet to the app.
Understanding the Camera Flow
Here’s how the camera integration works:
- User taps camera button in create task dialog.
- CameraPermissionHandler checks permission status.
- If granted, CameraScreen opens with live preview.
- User captures photo, which is saved and Uri returned.
- If permission denied, error state is shown.
In the next segment, you’ll convert this photo to binary data and pass it to Swift for processing.
Key Takeaways
In this segment, you’ve:
- Added CameraX and permission handling dependencies.
- Configured camera permissions in AndroidManifest.xml.
- Created a reusable permission handler composable.
- Built a full-screen camera preview with capture functionality.
- Understood why camera code lives in the Android layer.
Your app can now capture photos, but they aren’t processed or stored yet. In the next segment, you’ll learn how to convert image data to a format that Swift can process.