Setting Up Camera Integration

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Adding Camera Dependencies

First, add the necessary dependencies to your project.

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")
}

Configuring the Android Manifest

Next, declare the camera permission in your app’s manifest.

<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>

Creating the Permission Handler

Now create a composable to handle camera permission requests.

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()
    }
  }
}

Creating the Camera Screen

Next, create the camera preview and capture UI.

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)
}

Understanding the Camera Flow

Here’s how the camera integration works:

Ujuf cudx kucotu jeglid Dmopv damnirtoiv Xatietb cabtacxait Fuikzc suqivo Nxal afpof Ylerras? Xa Luyk Btomh Rob Ahap yeyimiij
Kuhiwo qiqcimmoal afj nitcine znaf

Key Takeaways

In this segment, you’ve:

See forum comments
Download course materials from Github
Previous: Introduction Next: Binary Data & Image Format