Blog Infos
Author
Published
Topics
, , , ,
Published

Over the past year, Compose Multiplatform has made impressive strides in bringing together different parts of Jetpack Compose, like lifecycle and ViewModel, into common part of KMP. But let’s be real — features like ExoPlayer and Camera aren’t quite in the common code yet. And honestly, that’s not a bad thing. Sometimes, the best solution is to use platform-specific libraries tailored to the job rather than forcing everything into a one-size-fits-all approach.

Now, if you’d rather skip the nitty-gritty of implementing camera features and just want to stick to writing common code, I’ve got you covered. I’ve built a library with a simple API that lets you create camera apps without the hassle. It seamlessly uses AVCamera for iOS and CameraX for Android behind the scenes, so you can stay focused on your code and let the library handle the platform specifics.

Let’s start using the library to build a simple camera app. Get the latest version from the repo.

GitHub – Kashif-E/CameraK: A camera library for Compose Multiplatform
A camera library for Compose Multiplatform. Contribute to Kashif-E/CameraK development by creating an account on…

github.com

At the time of writing this article, the latest version is 0.0.7. Let’s add it to the commonMain

    commonMain.dependencies {
            implementation("io.github.kashif-mehmood-km:camerak:0.0.5")
        }

once you add the library now sync the project.

Showing Preview:

After syncing is completed go to your composable where you want to create the camera preview.

First, we will need to create a CameraController which will be null initially.

val cameraController = remember { mutableStateOf<CameraController?>(null) }

The camera controller is an expected class that controls different features of the camera library.

CameraPreview(modifier = Modifier.fillMaxSize(), cameraConfiguration = {
    setCameraLens(CameraLens.BACK)
    setFlashMode(FlashMode.OFF)
    setImageFormat(ImageFormat.JPEG)
    setDirectory(Directory.PICTURES)
    addPlugin(/**Plugin here**/)

}, onCameraControllerReady = {
    cameraController.value = it
    println("Camera Controller Ready ${cameraController.value}")
   
})

cameraController.value?.let { controller ->
    CameraScreen(cameraController = controller, imageSaverPlugin)
}

CameraKPreview is a Composable function which takes three parameters a modifier to modify size, width, height and other aspects of the camera preview, cameraConfiguration which will be used to create the camera controller and onCameraControllerReady callback returns the controller once it has been built with the configuration.CameraK has a plugin-based API which means you can enhance its capabilities with existing plugins or create your own by implementing the CameraPlugin interface. Currently, there are two plugins that we have one is for saving images locally and the other is for QR scanning, more on that later.

Now, run the app….

Your app won’t work because we need to get permission from Android and IOS mainly Camera and External Storage permission on Android, NSCameraUsageDescription and NSPhotoLibraryUsageDescription for IOS.

For Android add these to the manifest.

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

For IOS add these to Info.plist

<key>NSCameraUsageDescription</key>
<string>Camera permission is required for the app to work.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo Library permission is required for the app to work.</string>

Now, getting back to the code the library contains the required code to check the permissions and ask for required permissions as well.

// Initialize Camera Permission State based on current permission status  
val cameraPermissionState = remember {
    mutableStateOf(
        permissions.hasCameraPermission()
    )
}

// Initialize Storage Permission State  
val storagePermissionState = remember {
    mutableStateOf(
        permissions.hasStoragePermission()
    )
}

if (!cameraPermissionState.value) {
    permissions.RequestStoragePermission(onGranted = { cameraPermissionState.value = true },
        onDenied = {
            println("Camera Permission Denied")
        })
}


if (!storagePermissionState.value) {
    permissions.RequestStoragePermission(onGranted = { storagePermissionState.value = true },
        onDenied = {
            println("Storage Permission Denied")
        })
}

// Initialize CameraController only when permissions are granted  
if (cameraPermissionState.value && storagePermissionState.value) {
CameraPreview(modifier = Modifier.fillMaxSize(), cameraConfiguration = {
    setCameraLens(CameraLens.BACK)
    setFlashMode(FlashMode.OFF)
    setImageFormat(ImageFormat.JPEG)
    setDirectory(Directory.PICTURES)
    addPlugin(/**Plugin here**/)

}, onCameraControllerReady = {
    cameraController.value = it
    println("Camera Controller Ready ${cameraController.value}")
  
})
cameraController.value?.let { controller ->
    CameraScreen(cameraController = controller, imageSaverPlugin)
}

once done, run the app and you should see the permission pop up.

Allow the permission and you should be able to see the preview

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Sharing code across platforms is a wonderful superpower. But sometimes, sharing 100% of your codebase isn’t the goal. Maybe you’re migrating existing apps to multiplatform, maybe you have platform-specific libraries or APIs you want to…
Watch Video

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Developer

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform ...

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Deve ...

Jobs

Taking and Saving Pictures:

To take pictures, let’s add a a button

 val scope = rememberCoroutineScope()
// Capture Button at the Bottom Center  
        Button(
            onClick = {
                scope.launch {
                    when (val result = cameraController.takePicture()) {
                        is ImageCaptureResult.Success -> {

                           val  imageBitmap = result.byteArray.decodeToImageBitmap()
                           
                        }

                        is ImageCaptureResult.Error -> {
                            println("Image Capture Error: ${result.exception.message}")
                        }
                    }
                }
            }, modifier = Modifier.size(70.dp).clip(CircleShape).align(Alignment.BottomCenter)

        ) {
            Text(text = "Capture")
        }

we can take the image using takePicture function which takes one parameter ImageFormat which is an enum and has PNG and JPEG values for now. takePicture is a suspend function and should be called from a Coroutine, it then returns an object ImageCaptureResult.

ImageCaptureResult is a sealed class that can be result in either success or error.

  coroutineScope.launch {
                    val imageResult = controller.takePicture(ImageFormat.PNG)

                    when (imageResult) {
                        is ImageCaptureResult.Error -> {

                        }

                        is ImageCaptureResult.Success -> {

                        }
                    }
                }

If the result is an error you can get the message from imageResult and if the result is success it will have

data class Success(val image: ByteArray, val path: String)

and image which is a ByteArray and then the path where the image is saved.

you can now use the image byteArray to convert it to ImageBitmap or ImageVector and display it using the Image composable.

   imageResult.image.decodeToImageBitmap()
   imageResult.image.decodeToImageVector()

Why did i choose to return byteArray? The reason is if you uploading to a cloud storage such as Amazon S3 you can directly send the byte array and it will be able to handle it without an issue and you can easily convert it to image vector or image bitmap and use it inside your composables.

If you want to save it to local storage. I have got you there as well either you can use any other library or your code to convert the byte array to a file and save that or you can use the built-in function.

you need to add a CameraKplugin for this

implementation("io.github.kashif-mehmood-km:image_saver_plugin:0.0.1")

Once you add the plugin sync the project, go back to you code and create a plugin object:

val imageSaverPlugin = createImageSaverPlugin(
    config = ImageSaverConfig(
        isAutoSave = false, // Set to true to enable automatic saving  
        prefix = "MyApp", // Prefix for image names when auto-saving  
        directory = Directory.PICTURES, // Directory to save images  
        customFolderName = "CustomFolder" // Custom folder name within the directory, only works on android for now  
    )
)

Config can be customized according to the requirements.

Next, you need to add the plugin to cameraController.

CameraPreview(modifier = Modifier.fillMaxSize(), cameraConfiguration = {
    setCameraLens(CameraLens.BACK)
    setFlashMode(FlashMode.OFF)
    setImageFormat(ImageFormat.JPEG)
    setDirectory(Directory.PICTURES)
//** add plugin here **//
    addPlugin(imageSaverPlugin)
}, onCameraControllerReady = {
    cameraController.value = it
    println("Camera Controller Ready ${cameraController.value}")
    
})

You can check the complete code here, along with the setup for QRScannerPlugin.

https://github.com/Kashif-E/CameraK/blob/main/Sample/src/commonMain/kotlin/org/company/app/App.kt?source=post_page—–ec92cb944ec5——————————–#L88

Switching Camera:

Let’s add another button to switch our camera lenses.

 Button(onClick = {controller.toggleCameraLens() }) {
   Text("Toggle Camera Lens")
 }

Inside the onClick lambda we need to call a function using the controller.

controller.toggleCameraLens()

and it will look like this.

Button(onClick = {controller.toggleCameraLens() }) {
   Text("Toggle Camera Lens")
 }
Switching Flash Mode:

Just like flash Mode, we need to create another button/switch to toggle flash mode. We will be using a switch so we first need to create a state for flash mode tracking

val flashMode = remember(controller.getFlashMode()) {
        controller.getFlashMode() == FlashMode.ON
    }

Now let’s add a switch so we can use it to turn on/off the camera flash.

Switch(
       checked = flashMode,
       onCheckedChange = { controller.toggleFlashMode() }
)

as simple as that.

Complete Code for Camera Features:
@OptIn(ExperimentalResourceApi::class, ExperimentalUuidApi::class)
@Composable
fun CameraScreen(cameraController: CameraController, imageSaverPlugin: ImageSaverPlugin) {
    val scope = rememberCoroutineScope()
    var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
    var isFlashOn by remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .fillMaxSize()
    ) {

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .align(Alignment.TopStart),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            // Flash Mode Switch
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(text = "Flash")
                Spacer(modifier = Modifier.width(8.dp))
                Switch(
                    checked = isFlashOn,
                    onCheckedChange = {
                        isFlashOn = it
                        cameraController.toggleFlashMode()
                    }
                )
            }

            // Camera Lens Toggle Button
            Button(onClick = { cameraController.toggleCameraLens() }) {
                Text(text = "Toggle Lens")
            }
        }

        // Capture Button at the Bottom Center
        Button(
            onClick = {
                scope.launch {
                    when (val result = cameraController.takePicture()) {
                        is ImageCaptureResult.Success -> {

                            imageBitmap = result.byteArray.decodeToImageBitmap()
                            // If auto-save is disabled, manually save the image
                            if (!imageSaverPlugin.config.isAutoSave) {
                                // Generate a custom name or use default
                                val customName = "Manual_${Uuid.random().toHexString()}"

                                imageSaverPlugin.saveImage(
                                    byteArray = result.byteArray,
                                    imageName = customName
                                )
                            }
                        }

                        is ImageCaptureResult.Error -> {
                            println("Image Capture Error: ${result.exception.message}")
                        }
                    }
                }
            },
            modifier = Modifier
                .size(70.dp)
                .clip(CircleShape)
                .align(Alignment.BottomCenter)

        ) {
            Text(text = "Capture")
        }

        // Display the captured image
        imageBitmap?.let { bitmap ->
            Image(
                bitmap = bitmap,
                contentDescription = "Captured Image",
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            )

            LaunchedEffect(bitmap) {
                delay(3000)
                imageBitmap = null
            }
        }
    }
}

That’s a wrap for this article! I encourage you to give the library a try — it’s still in a very experimental phase, so your feedback is appreciated. Whether it’s issues, feature requests, or suggestions, I’d love to hear from you.

If you do give it a go, let me know how it works for you. Your support, whether through claps or starring the repository, would mean the world to me and will motivate me to keep developing new features and improving what’s already there.

https://github.com/kashif-e/CameraK?source=post_page—–ec92cb944ec5——————————–

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu