Skip to content

ismoy/ImagePickerKMP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

300 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ImagePickerKMP

Cross-platform Image Picker & Camera Library for Kotlin Multiplatform

Easily capture or select images on Android, iOS, Desktop, and Web — all with a single API.
Built with Compose Multiplatform, designed for simplicity, performance, and flexibility.

ImagePickerKMP Banner

CI Code Coverage License Kotlin All Contributors

Maven Central NPM Version NPM Downloads GitHub Release GitHub Repo stars GitHub last commit

Compose Multiplatform Android iOS Desktop JavaScript WebAssembly Detekt


Documentation Site   Sponsor

ImagePickerKMP saves you 2 weeks of native Android/iOS/Web integration work.
It's free and open source. If your app or company benefits from it, consider sponsoring to keep it maintained and updated with every new KMP/Compose release.
→ Become a sponsor


Example

Complete Example App

ImagePickerKMP-Example →

Full-featured sample application showcasing:

  • All library features and configurations

Quick Start

⚠️ Requirements

Requirement Minimum version
Kotlin 2.3.20 (breaking change — see CHANGELOG)
Compose Multiplatform 1.10.3
Ktor 3.4.1
Android minSdk 24
Android compileSdk 36

Note: This library is compiled with Kotlin 2.3.20. Projects using Kotlin < 2.3.x will get an ABI incompatibility error at compile time. If you need Kotlin 2.1.x support, use a previous version of this library.

Installation

Kotlin Multiplatform:

dependencies {
    implementation("io.github.ismoy:imagepickerkmp:1.0.41")
}

React/JavaScript:

npm install imagepickerkmp

New — rememberImagePickerKMP (recommended)

The modern, idiomatic Compose API. A single state holder — no manual booleans, no Render() call needed.

@Composable
fun basicUsageScreen() {
    val picker = rememberImagePickerKMP(
        config = ImagePickerKMPConfig(
            galleryConfig = GalleryConfig(
                allowMultiple = true,
                selectionLimit = 20
            )
        )
    )
    val result = picker.result

    Scaffold(
        modifier = Modifier
            .fillMaxSize(),
        topBar = {
            TopAppBar(
                title = { Text("Basic Usage") },
                navigationIcon = {
                    IconButton(onClick = {}) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "Go back"
                        )
                    }
                }
            )
        },
        bottomBar = {
            BottomAppBar {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 16.dp),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Button(
                        onClick = { picker.launchCamera() },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("Camera")
                    }
                    Button(
                        onClick = { picker.launchGallery() },
                        modifier = Modifier.weight(1f)
                    ) {
                        Text("Gallery")
                    }
                }
            }
        }
    ){scaffoldPadding->
        Column(
            modifier = Modifier
                .padding(scaffoldPadding)
                .fillMaxSize()
        ) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),
                contentAlignment = Alignment.Center
            ) {
                when (result) {

                    is ImagePickerResult.Loading -> {
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally,
                            modifier = Modifier.padding(16.dp)
                        ) {
                            CircularProgressIndicator()
                            Text(
                                text = "Loading...",
                                color = Color.Gray,
                                modifier = Modifier.padding(top = 12.dp)
                            )
                        }
                    }

                    is ImagePickerResult.Success -> {
                        // Result here
                    }

                    is ImagePickerResult.Error -> {
                        Text(
                            text = "Error: ${result.exception.message}",
                            color = Color.Red,
                            modifier = Modifier.padding(16.dp)
                        )
                    }

                    is ImagePickerResult.Dismissed -> {
                        Text("Selection cancelled", color = Color.Gray)
                    }

                    is ImagePickerResult.Idle -> {
                        Text("Press a button to get started", color = Color.Gray)
                    }
                }
            }
        }
    }
}

Per-launch overrides:

// Override gallery options for a single launch
picker.launchGallery(
    allowMultiple = true,
    selectionLimit = 5,
    mimeTypes = listOf(MimeType.IMAGE_JPEG),
    includeExif = true
)

// Override camera options for a single launch
picker.launchCamera(
    cameraCaptureConfig = CameraCaptureConfig(compressionLevel = CompressionLevel.HIGH),
    enableCrop = false
)

API — rememberImagePickerKMP

The library exposes a single public API based on the standard Compose state-hook pattern:

@Composable
fun MyScreen() {
    val picker = rememberImagePickerKMP()
    val result = picker.result

    Row(
        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Button(onClick = { picker.launchCamera() }, modifier = Modifier.weight(1f)) {
            Text("Camera")
        }
        Button(onClick = { picker.launchGallery() }, modifier = Modifier.weight(1f)) {
            Text("Gallery")
        }
    }

    when (result) {
        is ImagePickerResult.Loading   -> CircularProgressIndicator()
        is ImagePickerResult.Success   -> {
            val photos = result.photos
            if (photos.size == 1) {
                CameraResultCard(photo = photos.first())
            } else {
                MultiPhotoGrid(photos = photos)
            }
        }
        is ImagePickerResult.Error     -> Text("Error: ${result.exception.message}", color = Color.Red)
        is ImagePickerResult.Dismissed -> Text("Selection cancelled", color = Color.Gray)
        is ImagePickerResult.Idle      -> Text("Press a button to get started", color = Color.Gray)
    }
}

Configuration

Pass an ImagePickerKMPConfig to customize behavior globally:

val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cropConfig          = CropConfig(enabled = true, squareCrop = true),
        galleryConfig       = GalleryConfig(allowMultiple = true, selectionLimit = 10),
        cameraCaptureConfig = CameraCaptureConfig(
            compressionLevel = CompressionLevel.HIGH,
            includeExif      = true
        )
    )
)

Per-launch overrides

Override any parameter for a single invocation without changing the global config:

// Allow multiple selection only for this launch
picker.launchGallery(allowMultiple = true, selectionLimit = 5)

// Enable EXIF only for this camera launch
picker.launchCamera(cameraCaptureConfig = CameraCaptureConfig(includeExif = true))

Key Features

  • rememberImagePickerKMP — New idiomatic API: single state holder, launchCamera() / launchGallery() with per-launch overrides, reactive result via ImagePickerResult (Idle → Loading → Success/Dismissed/Error). No Render(), no manual booleans.
  • Cross-platform — Android, iOS, Desktop, Web
  • Camera & Gallery — Direct access with unified API
  • Image Cropping — Built-in crop functionality
  • Smart Compression — Configurable quality levels
  • EXIF Metadata — GPS, camera info, timestamps (Android/iOS)
  • PDF Support — Select PDF documents alongside images
  • Extension Functions — Easy image processing (loadPainter(), loadBytes(), loadBase64())
  • Permission Handling — Automatic permission management
  • Async Processing — Non-blocking UI with coroutines
  • Format Support — JPEG, PNG, HEIC, HEIF, WebP, GIF, BMP, PDF

Platform Support

Platform Minimum Version Camera Gallery Crop EXIF Status
Android API 21+
iOS iOS 12.0+
Desktop JDK 11+
JS/Web Modern Browsers
Wasm/Web Modern Browsers

Live Demos

Online Demos

View Interactive Demos →

Experience ImagePickerKMP in action:

  • Mobile Demos - Android & iOS camera/gallery functionality
  • Desktop Demo - File picker and image processing
  • Web Demo - React integration with WebRTC camera
  • Crop Demo - Interactive image cropping across platforms

Documentation

Complete Guides

Resource Description
Integration Guide Complete setup and configuration
Customization Guide UI customization and theming
React Guide Web development setup
Permissions Guide Platform permissions
API Reference Complete API documentation

Advanced Configuration

Image Compression

val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cameraCaptureConfig = CameraCaptureConfig(
            compressionLevel = CompressionLevel.HIGH // LOW, MEDIUM, HIGH
        ),
        permissionAndConfirmationConfig = PermissionAndConfirmationConfig(
            skipConfirmation = true
        )
    )
)
picker.launchCamera()

EXIF Metadata Extraction (Camera)

val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        cameraCaptureConfig = CameraCaptureConfig(
            includeExif = true  // Android/iOS only
        )
    )
)

when (val result = picker.result) {
    is ImagePickerResult.Success -> {
        result.photos.first().exif?.let { exif ->
            println(" Location: ${exif.latitude}, ${exif.longitude}")
            println(" Camera: ${exif.cameraModel}")
            println(" Taken: ${exif.dateTaken}")
        }
    }
    else -> Unit
}

EXIF Metadata Extraction (Gallery)

val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        galleryConfig = GalleryConfig(
            allowMultiple = true,
            mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
            includeExif = true  // Android/iOS only
        )
    )
)
picker.launchGallery()

Multiple Selection with Filtering

// Images only
val picker = rememberImagePickerKMP(
    config = ImagePickerKMPConfig(
        galleryConfig = GalleryConfig(
            allowMultiple = true,
            mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG)
        ),
        cropConfig = CropConfig(enabled = true)
    )
)
picker.launchGallery()

// Images and PDFs
picker.launchGallery(
    mimeTypes = listOf(
        MimeType.IMAGE_JPEG,
        MimeType.IMAGE_PNG,
        MimeType.APPLICATION_PDF  // PDF support
    )
)

iOS Permissions Setup

Add to your Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera access needed to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access needed to select images</string>

Extension Functions

Process images easily with built-in extension functions:

val picker = rememberImagePickerKMP()

when (val result = picker.result) {
    is ImagePickerResult.Success -> {
        val photo = result.photos.first()

        val imageBytes = photo.loadBytes()        // ByteArray for file operations
        val imagePainter = photo.loadPainter()    // Painter for Compose UI
        val imageBitmap = photo.loadImageBitmap() // ImageBitmap for graphics
        val imageBase64 = photo.loadBase64()      // Base64 string for APIs

        // File system operations (kotlinx-io)
        val absolutePath = photo.absolutePath     // String - absolute file path
        val path = photo.asPath()                 // Path object for file operations
        val exists = photo.exists()               // Check if file exists
        val rawSource = photo.asRawSource()       // RawSource for low-level reading
        val source = photo.asSource()             // Buffered Source for efficient reading

        // Copy photo to another location
        val sink = SystemFileSystem.sink(Path("copy.jpg"))
        photo.transferToSink(sink)                // Transfer content to RawSink
    }
    else -> Unit
}

React/Web Integration

ImagePickerKMP is available as an NPM package for web development:

npm install imagepickerkmp

Features:

  • WebRTC Camera Access - Mobile & desktop camera support
  • TypeScript Support - Full type definitions included
  • Drag & Drop - File picker with drag and drop
  • React Components - Ready-to-use React components
  • Cross-Framework - Works with React, Vue, Angular, Vanilla JS

Smart Gallery vs File Explorer Detection

  • Images: Opens native Android gallery for photos
  • PDFs: Opens file explorer for document access
  • Mixed Types: Automatically chooses best picker for content type
  • Automatic Detection: No configuration needed - works out of the box!

Complete React Integration Guide →

⚠️ Known Issues & Troubleshooting

iOS build fails with _OBJC_CLASS_$_CLLocation linker error

If your iOS build fails with:

ld: Undefined symbols: _OBJC_CLASS_$_CLLocation
linker command failed with exit code 1

Android and JVM Desktop work fine, but iOS fails during the linking phase.

Fix: Add CoreLocation.framework manually in Xcode:

  1. Select your app target → Build Phases → Link Binary With Libraries
  2. Click +, search for CoreLocation, and click Add
  3. Clean (⇧⌘K) and rebuild

No code changes needed. See FAQ and Integration Guide for full details.

Support & Contributing


Sponsors & Backers

ImagePickerKMP is free and open source. Maintaining it across Android, iOS, Desktop, Web and WASM with every Kotlin/Compose Multiplatform release takes real time and effort.

If this library saves you time or money in production, please consider supporting it:

Tier Amount Benefit
☕ Coffee $5/mo Name in the backers list
🥈 Silver $25/mo Logo in README + priority issue response

Sponsor

Current Sponsors

james-codersHT
james-codersHT

Sponsors get their name/logo displayed here. → Become a sponsor


Contributors

Thanks to these wonderful people (emoji key):

ismoy
ismoy

💻 📖 🚧 🎨 🤔
medAndro
medAndro

💻 🐛
YaminMahdi
YaminMahdi

💻
jadlr
jadlr

💻
daniil-pastuhov
daniil-pastuhov

💻
fanqieVip
fanqieVip

💻

This project follows the all-contributors specification. Contributions of any kind welcome!


Made with ❤️ for the Kotlin Multiplatform community
Star this repo if it helped you!

Sponsor this project

 

Packages

 
 
 

Contributors

Languages