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.
- DSL-based Navigation Graph — Declare screens inside
ImagePickerNavHostlikeNavHost - 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
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'
}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.
ImagePicker is built around three principles:
Screens are declared inside ImagePickerNavHost { }, just like Jetpack Navigation's NavHost:
ImagePickerNavHost(state = state) {
ImagePickerScreen(...)
PreviewScreen { ... }
}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 |
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.selectedMediaContentsA 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.selectedMediaContentsRenders 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)
}
)
}
}
}
}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) }
)
}
}
}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 aclickablemodifier) 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)
)
}
}
}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:
PreviewScreenmust be explicitly declared insideImagePickerNavHost.
Omitting it while callingonNavigateToPreviewScreen()from a cell will cause a runtime crash.
ImagePickerNavHostState holds picker configuration and exposes the selection result to your app.
| Parameter | Description |
|---|---|
max |
Maximum number of media items that can be selected |
| 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




