Common questions and answers about ImagePickerKMP.
ImagePickerKMP is a modern, cross-platform image picker library for Kotlin Multiplatform (KMP) that provides seamless camera integration for both Android and iOS platforms.
Key Features:
-
Cross-platform camera integration
-
Smart permission handling
-
Customizable UI components
-
High-quality photo capture
-
Comprehensive error handling
- Android: API 21+ (Android 5.0+)
- iOS: iOS 12.0+
- Kotlin Multiplatform: Full support
Android:
- Minimum SDK: API 21
- Kotlin: 1.8+
- Compose Multiplatform: 1.4+
iOS:
- iOS: 12.0+
- Xcode: 14+
- Kotlin Multiplatform: 1.8+
Yes, ImagePickerKMP is open-source and free to use under the MIT License. You can use it in both personal and commercial projects.
Advantages:
-
Cross-platform with single codebase
-
Modern Compose Multiplatform UI
-
Smart permission handling
-
Customizable components
-
Active development and support
Compared to alternatives:
- More modern than CameraX (Android-only)
- More integrated than UIImagePickerController (iOS-only)
- Better permission handling than most alternatives
- Cross-platform advantage over platform-specific solutions
Add the dependency to your build.gradle.kts:
dependencies {
implementation("io.github.ismoy:imagepickerkmp-photo:1.1.0")
// ... add other modules as needed (video, audio, scanner)
}Android (AndroidManifest.xml):
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />iOS (Info.plist):
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to capture photos</string>For basic usage, no additional configuration is required. The library handles most setup automatically.
For advanced features, you may need to:
-
Configure custom themes
-
Add custom permission dialogs
-
Configure photo capture preferences
-
Add to your iOS project:
# Podfile target 'YourApp' do use_frameworks! pod 'ImagePickerKMP', :path => '../path/to/your/library' end
-
Run pod install:
pod install
-
Import in your iOS code:
import ImagePickerKMP
@Composable
fun MyImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
println("Photo captured: ${result.photos.first()}")
}
is ImagePickerResult.Error -> {
println("Error: ${result.exception.message}")
}
is ImagePickerResult.Dismissed -> {
println("User cancelled or dismissed the picker")
}
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}The library handles permissions automatically, but you can customize the behavior:
@Composable
fun CustomPermissionHandler() {
RequestCameraPermission(
titleDialogConfig = "Camera Permission Required",
descriptionDialogConfig = "Please enable camera access",
btnDialogConfig = "Open Settings",
onPermissionPermanentlyDenied = {
// Handle permanent denial
},
onResult = { granted ->
// Handle permission result
}
)
}@Composable
fun CustomImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
preference = CapturePhotoPreference.HIGH_QUALITY,
permissionAndConfirmationConfig = PermissionAndConfirmationConfig(
customPermissionHandler = { config ->
// Custom permission handling
},
customConfirmationView = { result, onConfirm, onRetry ->
// Custom confirmation view
}
)
)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Handle captured photo
}
is ImagePickerResult.Error -> {
// Handle error
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}@Composable
fun HighQualityImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
preference = CapturePhotoPreference.HIGH_QUALITY
)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Handle high quality photo
}
is ImagePickerResult.Error -> {
// Handle errors
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Capture High Quality")
}
}The library provides an ImagePickerResult.Dismissed state that is emitted when the user cancels or dismisses the picker without selecting anything. This is essential for resetting your UI state.
@Composable
fun MyImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
println("Photo captured: ${result.photos.first()}")
}
is ImagePickerResult.Error -> {
println("Error: ${result.exception.message}")
}
is ImagePickerResult.Dismissed -> {
println("User cancelled or dismissed the picker")
}
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}@Composable
fun MyGalleryPicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(allowMultiple = true)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
println("Selected ${result.photos.size} images")
}
is ImagePickerResult.Error -> {
println("Error: ${result.exception.message}")
}
is ImagePickerResult.Dismissed -> {
println("User cancelled gallery selection")
}
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchGallery(allowMultiple = true) }) {
Text("Pick from Gallery")
}
}When ImagePickerResult.Dismissed is emitted:
- Android: User cancels the selection dialog or camera interface
- iOS: User taps "Cancel" in the dialog or camera interface
- iOS: User cancels camera permission request
- iOS: User cancels the camera interface (taps "Cancel" or "X")
Similarities:
- Same API interface
- Same permission handling
- Same error handling
- Same customization options
Differences:
- Android uses CameraX, iOS uses AVFoundation
- Permission flow slightly different (iOS shows settings immediately after first denial)
- Some platform-specific optimizations
@Composable
fun PlatformSpecificImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Platform-agnostic handling
}
is ImagePickerResult.Error -> {
when (result.exception) {
is CameraPermissionException -> {
// Handle permission errors
}
is PhotoCaptureException -> {
// Handle capture errors
}
else -> {
// Handle other errors
}
}
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}iOS-specific features are handled internally by the library. You don't need to write platform-specific code for most use cases.
For advanced iOS features:
// iOS-specific configuration
@Composable
fun IOSImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// iOS-specific handling
}
is ImagePickerResult.Error -> {
// iOS-specific error handling
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}- Permissions: Ensure camera permission is granted
- Hardware: Check if device has camera
- Lifecycle: Check if component is in correct lifecycle state
- Dependencies: Verify all dependencies are properly added
- Check manifest: Ensure camera permission is declared
- Check Info.plist: Ensure NSCameraUsageDescription is set (iOS)
- Check implementation: Ensure RequestCameraPermission is used
- Check platform: Verify platform-specific setup
- Hardware: Device may not have camera
- Permissions: Camera permission may be denied
- Camera in use: Another app may be using camera
- Simulator: Camera not available in simulator (use device)
- No camera app: On Android the capture goes through the system camera app, which can be absent or disabled (enterprise/kiosk devices, or a user disabling the stock camera)
result is sticky: ImagePickerResult.Error persists until you either start another launch
(which sets Loading) or clear it explicitly. Call reset() once you've shown the error:
is ImagePickerResult.Error -> {
ErrorBanner(
message = result.exception.message,
onDismiss = { picker.reset() } // back to Idle
)
}Call reset() from an event handler as above, not directly inside the when block — writing
state during composition is what you want to avoid.
This applies to every error, not just an unavailable camera — a MIME-type mismatch or a failed gallery read behaves the same way. A dismissal that follows an error does not clear it, so "no camera app" stays distinguishable from "user cancelled".
- Memory issues: Use image compression for large photos
- Lifecycle issues: Ensure proper lifecycle management
- Exception handling: Add proper error handling
@Composable
fun RobustImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
try {
// Process photo safely
processPhoto(result.photos.first())
} catch (e: Exception) {
// Handle processing errors
showError("Failed to process photo: ${e.message}")
}
}
is ImagePickerResult.Error -> {
// Handle capture errors
showError("Camera error: ${result.exception.message}")
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}// Debug permission status
fun debugPermissions(context: Context) {
val hasPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
println("Camera permission granted: $hasPermission")
}Symptoms:
Could not find or use auto-linked framework '_LocationEssentials': framework '_LocationEssentials' not found
ld: Undefined symbols:
_OBJC_CLASS_$_CLLocation, referenced from:
in ComposeApp[...](libImagePickerKMP:library-cache.a.o)
linker command failed with exit code 1
Android and JVM Desktop work fine, but the iOS build fails during the linking phase.
Cause:
ImagePickerKMP uses CoreLocation internally (e.g. for location metadata when capturing images). On some Xcode / KMP configurations the framework is not auto-linked, so the linker cannot resolve CLLocation symbols.
Solution:
Add CoreLocation.framework manually to your Xcode project's Build Phases:
- Open your iOS project (
.xcworkspaceor.xcodeproj) in Xcode. - Select your app target in the Project navigator.
- Go to Build Phases → Link Binary With Libraries.
- Click + and search for CoreLocation.
- Select
CoreLocation.frameworkand click Add. - Clean the build folder (Product → Clean Build Folder, or
⇧⌘K) and rebuild.
✅ No code changes are required — this is a project-level configuration step only.
Reported environment: Xcode 15.2 · iOS 15 · Kotlin 2.2.x · ImagePickerKMP 1.0.35
- Use URIs instead of Bitmaps:
@Composable
fun MemoryEfficientImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
var imageUri by remember { mutableStateOf<Uri?>(null) }
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Store URI instead of Bitmap
imageUri = result.photos.first().uri
}
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
// Load image only when needed
imageUri?.let { uri ->
AsyncImage(
model = uri,
contentDescription = "Captured photo"
)
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}- Use image compression:
@Composable
fun CompressedImagePicker() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.MEDIUM // LOW | MEDIUM | HIGH
),
galleryConfig = GalleryConfig(
compressionLevel = CompressionLevel.MEDIUM // set separately for gallery
)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Image is compressed
val photo = result.photos.first()
}
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Compressed Photo")
}
}- Use FAST preference:
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
preference = CapturePhotoPreference.FAST
)
)
)
Button(onClick = { picker.launchCamera() }) {
Text("Quick Capture")
}- Pre-initialize camera:
// Pre-initialize camera in background
LaunchedEffect(Unit) {
initializeCamera()
}@Composable
fun LargePhotoHandler() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
lifecycleScope.launch(Dispatchers.IO) {
// Process large photo in background
val processedImage = processLargeImage(result.photos.first())
withContext(Dispatchers.Main) {
// Update UI with processed image
}
}
}
is ImagePickerResult.Error -> { /* handle error */ }
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}@Composable
fun CustomPermissionDialog(
title: String,
description: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { Text(description) },
confirmButton = {
Button(onClick = onConfirm) {
Text("Grant Permission")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}@Composable
fun CustomConfirmationView(
result: PhotoResult,
onConfirm: () -> Unit,
onRetry: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize()
) {
// Photo preview
AsyncImage(
model = result.uri,
contentDescription = "Captured photo",
modifier = Modifier.weight(1f)
)
// Action buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = onRetry) {
Text("Retry")
}
Button(onClick = onConfirm) {
Text("Use Photo")
}
}
}
}@Composable
fun ThemedImagePicker() {
val customTheme = remember {
MaterialTheme.colors.copy(
primary = Color(0xFF1976D2),
secondary = Color(0xFF42A5F5)
)
}
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig()
)
MaterialTheme(colors = customTheme) {
when (val result = picker.result) {
is ImagePickerResult.Success -> {
// Handle photo capture
}
is ImagePickerResult.Error -> {
// Handle errors
}
is ImagePickerResult.Dismissed -> { /* cancelled */ }
is ImagePickerResult.Loading -> { /* loading */ }
is ImagePickerResult.Idle -> { /* initial state */ }
}
Button(onClick = { picker.launchCamera() }) {
Text("Take Photo")
}
}
}Question: The photo picker used to allow selecting PDFs. Why was this removed?
Answer: To keep the library as lightweight and performant as possible, we restructured it into multiple modules (imagepickerkmp-photo, imagepickerkmp-video, etc.). The photo module is now strictly a Photo Picker. Handling PDFs introduced unnecessary overhead and generic MIME types (*/*), which complicated permissions and memory handling. If you need document selection, it's recommended to use a dedicated file picker library.
Question: My app crashed with an OOM error when a user selected multiple 15MB photos from the gallery.
Answer: Yes, this is completely resolved in version 1.1.0+!
Previously, the library loaded the entire raw image byte array into memory just to calculate dimensions or apply compression. Now, it uses ContentResolver.openFileDescriptor and inSampleSize (downsampling) to read dimensions instantly and load a scaled-down version of the image directly from the stream. This reduces memory footprint to almost 0MB during the selection phase, making the library incredibly fast and safe for large photo selections on both Android and iOS.
Problem: Photos taken with the front camera appear with incorrect orientation (mirrored or rotated).
Cause: Android front cameras have a different orientation than rear cameras. The image is captured with an orientation that isn't natural for the user.
Solution: The library now includes automatic orientation correction for front camera photos. The system:
- Automatically detects if the photo was taken with the front camera
- Applies mirror correction only when necessary
- Maintains quality of the original image
- Is efficient - only processes when it really needs correction
This is completely automatic and requires no configuration.
- Documentation: README.md
- API Reference: API_REFERENCE.md
- Examples: EXAMPLES.md
- GitHub Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: belizairesmoy72@gmail.com
- Search existing issues: Check if the bug is already reported
- Create new issue: Use the bug report template
- Provide details: Include steps to reproduce, environment info, logs
- Follow up: Respond to maintainer questions
- Search existing issues: Check if the feature is already requested
- Create feature request: Use the feature request template
- Provide details: Include use case, proposed implementation
- Discuss: Engage in community discussions
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
See CONTRIBUTING.md for detailed guidelines.
Still have questions? Feel free to ask in our GitHub Discussions or contact us directly.