ImagePicker is a Jetpack Compose library for displaying and selecting media from the device gallery.
ImagePicker uses a declarative DSL structure to define screens within a navigation graph, similar to NavHost
and composable in Jetpack Navigation
- π§© DSL-based Navigation Graph: Declare screens inside
ImagePickerNavHost
likeNavHost
- π¦ Fully customizable UI for album selector, preview bar, image cells and preview screen
- ποΈ Multi-selection with drag gesture support
- π’ Visual selection order (e.g., 1st, 2nd...)
- π· Camera integration with optional auto-select after capture
- πΌοΈ Full Preview screen for selected images
- π Pagination for smooth loading of large image sets (via Paging 3)
- ποΈ Album-based grouping with dynamic filtering
Step 1. Add it in your root build.gradle at the end of repositories:
dependencyResolutionManagement {
...
repositories {
maven { url 'https://jitpack.io' }
}
}
Step 2. Add the dependency
dependencies {
implementation 'com.github.minsuk-jang:ImagePicker:1.0.15'
}
Make sure to include the following in your AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
Declare your image picker UI using ImagePickerNavHost
, just like NavHost
in Jetpack Navigation.
Each slot (albumTopBar
, previewTopBar
, cellContent
) and PreviewScreen
gives access to its own custom scope to help you build highly flexible UIs.
ImagePickerNavHost(state = state) {
ImagePickerScreen(
albumTopBar = { ... },
previewTopBar = { ... },
cellContent = { ... }
)
PreviewScreen {
// Full-screen preview UI
}
}
π· Example Output:
Each slot in ImagePickerScreen
or PreviewScreen
is powered by a custom scope. These scopes provide the necessary state and event handlers you need to build fully customized UIs.
Below is a breakdown of each slot, its associated scope, and what you can do inside it.
Use this slot to show album-related UI such as a dropdown or album selector. The ImagePickerAlbumScope
gives you access to:
Property / Function | Description |
---|---|
albums: List<Album> |
List of all albums on the device |
selectedAlbum: Album? |
Currently selected album |
onClick(album: Album) |
Select the given album |
Example usage:
albumTopBar = {
var expanded by remember { mutableStateOf(false) }
Box {
Text(
text = selectedAlbum?.name ?: "All Albums",
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) // Select the album
}
)
}
}
}
}
This slot allows you to preview currently selected media contents in a custom layout. The ImagePickerPreviewScope
gives you access to:
Property / Function | Description |
---|---|
selectedMediaContents: List<MediaContent> |
List of selected media content |
onDeselect(mediaContent: MediaContent) |
Deselect the given media content |
Example usage:
previewTopBar = {
Row {
selectedMediaContents.forEach { media ->
AsyncImage(
model = media.uri,
contentDescription = null,
modifier = Modifier.clickable {
onDeselect(media) // Deselect
}
)
}
}
}
This slot renders each image cell in the grid. Only the MediaContent
is provided - the rest is up to you.
Use this slot to:
- Display thumbnails
- Indicate selected state (e.g., with a badge or overlay)
- Navigate to the preview screen
The ImagePickerCellScope
gives you access to:
Property / Function | Description |
---|---|
mediaContent: MediaContent |
The media content represented by this cell |
onNavigateToPreviewScreen(mediaContent: MediaContent) |
Triggers navigation to the Preview Screen |
Example usage:
cellContent = {
Box(modifier = Modifier.clickable {
onNavigateToPreviewScreen(mediaContent)
}) {
AsyncImage(model = mediaContent.uri, contentDescription = null)
if (mediaContent.selected) {
Text(
text = "#${mediaContent.selectedOrder}",
modifier = Modifier.align(Alignment.TopEnd)
)
}
}
}
π‘ You can fully control the UI β whether it's adding badges, applying blur, or animating selection β by customizing this slot.
This slot allows you to define the full-screen preview UI for selected media content. The PreviewScreenScope
provides:
Property / Function | Description |
---|---|
mediaContent: MediaContent |
The currently visible media content |
onBack() |
Navigate back to the picker screen |
onToggleSelection(mediaContent: MediaContent) |
Selects or deselects the given media content |
β οΈ Important
PreviewScreen must be explicitly declared inside ImagePickerNavHost.
If omitted, calling onNavigateToPreviewScreen() from a cell will cause a runtime crash.
The ImagePickerNavHostState
stores shared selection state and picker configuration across ImagePickerScreen
and PreviewScreen
.
Parameter | Description |
---|---|
max |
Maximum number of media contents that can be selected |
autoSelectAfterCapture |
Whether to auto-select the image after camera shot |
Property | Type | Description |
---|---|---|
selectedMediaContents |
List<MediaContent> |
List of currently selected media contents |
You can read this state anywhere in your app to reflect selection results, UI updates, or submission logic:
val selected = state.selectedMediaContents
ImagePicker is designed around three key principles:
Screens are declared inside ImagePickerNavHost { ... }
just like NavHost
and composable in Jetpack Navigation.
ImagePickerNavHost(state) {
ImagePickerScreen(...)
PreviewScreen { ... }
}
Each UI slot receives a dedicated Scope that provides data, actions for full composability:
Slot | Scope Interface | Description |
---|---|---|
albumTopBar |
ImagePickerAlbumScope |
Provides album list & selection |
previewTopBar |
ImagePickerPreviewTopBarScope |
Shows selected media, toggles select |
cellContent |
ImagePickerCellScope |
Controls image cell UI & navigate to Preview Screen |
PreviewScreen |
PreviewScreenScope |
Full-screen preview screen actions |
ImagePickerNavHostState
gives access to current selections and configuration.
val selectedItems = state.selectedMediaContents
val maxSelectable = state.max
Most pickers are monolithic and hard to extend. ImagePicker provides navigation-level flexibility, enabling clean separation between:
- Image list & preview
- UI customization & logic
- App-level state & picker state This structure makes it easy to build an image picker that feels native to your app, not boxed-in.