Skip to content

minsuk-jang/ImagePicker

Repository files navigation

ImagePicker

API License: MIT

A fully customizable, DSL-based image picker for Jetpack Compose

ImagePicker is a Jetpack Compose library for displaying and selecting media from the device gallery.
It uses a declarative DSL structure to define screens within a navigation graph, similar to NavHost in Jetpack Navigation.


Features

  • DSL-based Navigation Graph — Declare screens inside ImagePickerNavHost like NavHost
  • Fully customizable UI — Control album selector, preview bar, image cells, and preview screen independently
  • Multi-selection with drag gesture — Long-press and drag to batch-select images
  • Visual selection order — Display selection index (1st, 2nd, ...) on each cell
  • Full preview screen — Swipeable full-screen preview for selected images
  • Pagination — Smooth loading of large galleries via Paging 3
  • Album filtering — Dynamic album-based grouping and switching

Installation

Step 1. Add JitPack to your root settings.gradle:

dependencyResolutionManagement {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

Step 2. Add the dependency:

dependencies {
    implementation 'com.github.minsuk-jang:ImagePicker:1.0.17'
}

Permissions

Add the appropriate permissions to your AndroidManifest.xml based on the target API level:

<!-- API 32 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- API 33 and above -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

You should request these permissions at runtime before launching ImagePickerNavHost.


Concept

ImagePicker is built around three principles:

1. Declarative Navigation DSL

Screens are declared inside ImagePickerNavHost { }, just like Jetpack Navigation's NavHost:

ImagePickerNavHost(state = state) {
    ImagePickerScreen(...)
    PreviewScreen { ... }
}

2. Scoped Slot APIs

Each UI slot receives a dedicated scope that exposes only the data and actions relevant to that screen:

Slot Scope Responsibility
albumTopBar ImagePickerAlbumScope Album list and selection
previewTopBar ImagePickerPreviewScope Selected media preview and deselection
cellContent ImagePickerCellScope Image cell UI and navigation to preview
PreviewScreen PreviewScreenScope Full-screen preview actions

3. Shared Picker State

ImagePickerNavHostState bridges the picker and your app, exposing the final selection result:

val state = rememberImagePickerNavHostState(max = 10)

// Read selected results anywhere in your composable
val selected = state.selectedMediaContents

Quick Start

A complete example from setup to reading results:

// 1. Create state
val state = rememberImagePickerNavHostState(max = 10)

// 2. Declare the picker
ImagePickerNavHost(state = state) {
    ImagePickerScreen(
        albumTopBar = {
            // Show album selector using 'albums', 'selectedAlbum', 'onClick'
        },
        previewTopBar = {
            // Show selected thumbnails using 'selectedMediaContents', 'onDeselect'
        },
        cellContent = {
            // Render each cell using 'mediaContent', 'onNavigateToPreviewScreen'
        }
    )

    PreviewScreen {
        // Full-screen preview using 'mediaContent', 'onBack', 'onToggleSelection'
    }
}

// 3. Read selected results
val selected = state.selectedMediaContents


Slot APIs

albumTopBarImagePickerAlbumScope

Renders the album selector UI. The scope provides:

Property / Function Description
albums: List<Album> All albums available on device
selectedAlbum: Album? Currently selected album
onClick(album: Album) Switch to the given album
albumTopBar = {
    var expanded by remember { mutableStateOf(false) }

    Box {
        Text(
            text = selectedAlbum?.name ?: "All",
            modifier = Modifier.clickable { expanded = true }
        )

        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            albums.forEach { album ->
                DropdownMenuItem(
                    text = { Text("${album.name} (${album.count})") },
                    onClick = {
                        expanded = false
                        onClick(album)
                    }
                )
            }
        }
    }
}

previewTopBarImagePickerPreviewScope

Renders selected media in a preview bar. The scope provides:

Property / Function Description
selectedMediaContents: List<MediaContent> Currently selected media
onDeselect(mediaContent: MediaContent) Deselect the given item
previewTopBar = {
    Row {
        selectedMediaContents.forEach { media ->
            AsyncImage(
                model = media.uri,
                contentDescription = null,
                modifier = Modifier.clickable { onDeselect(media) }
            )
        }
    }
}

cellContentImagePickerCellScope

Renders each image cell in the grid. The scope provides:

Property / Function Description
mediaContent: MediaContent The media item for this cell
onSelect() Toggle the selection state of this cell
onNavigateToPreviewScreen(mediaContent: MediaContent) Navigate to the full preview screen

Note: Selection toggling is no longer handled internally. You must call onSelect() yourself (e.g. inside a clickable modifier) to toggle the selection state.

cellContent = {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable { onSelect() }
    ) {
        AsyncImage(model = mediaContent.uri, contentDescription = null)

        if (mediaContent.selected) {
            Text(
                text = "${mediaContent.selectedOrder + 1}",
                modifier = Modifier.align(Alignment.TopEnd)
            )
        }
    }
}

PreviewScreenPreviewScreenScope

Renders the full-screen swipeable preview. The scope provides:

Property / Function Description
mediaContent: MediaContent The currently visible media item
onBack() Navigate back to the picker screen
onToggleSelection(mediaContent: MediaContent) Select or deselect the current item

Note: PreviewScreen must be explicitly declared inside ImagePickerNavHost.
Omitting it while calling onNavigateToPreviewScreen() from a cell will cause a runtime crash.


ImagePickerNavHostState

ImagePickerNavHostState holds picker configuration and exposes the selection result to your app.

Parameters

Parameter Description
max Maximum number of media items that can be selected

Properties

Property Type Description
selectedMediaContents List<MediaContent> Currently selected media items
val state = rememberImagePickerNavHostState(max = 10)

// Pass state to the picker
ImagePickerNavHost(state = state) { ... }

// Read results
val selected = state.selectedMediaContents

About

A fully customizable, DSL-based image picker for Jetpack Compose

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages