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
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 CameraK
plugin 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.