relax_image_picker 3.1.0
relax_image_picker: ^3.1.0 copied to clipboard
A WhatsApp-like media picker for Flutter: OS photo picker or in-app gallery grid, camera with video capture and playback, documents, preview and theming.
Relax Image Picker #
Features #
- ๐ฑ WhatsApp-style UX โ bottom-sheet interface with smooth animations
- ๐ผ๏ธ Two gallery modes โ the permission-free OS photo picker (default) or a WhatsApp-style in-app grid rendered right in the sheet
- ๐ท Camera integration โ capture photos and videos without leaving the picker
- ๐ฌ Camera + gallery together โ the camera page keeps a thumbnail strip you drag up to reveal the full grid;
cameraFirst: trueopens straight on it - ๐ Gallery-like gestures โ tap previews, a long press starts multi-selection (bubbles stay hidden until then)
- ๐ Document selection โ pick files from device storage, with recent-documents recall between sessions
- ๐๏ธ Full-screen preview โ review images and documents, and play videos (captured or picked) before confirming
- ๐๏ธ Optional compression โ shrink images on the fly
- ๐ Permission-free by default โ the OS-picker mode needs no
READ_MEDIA_*and no Google Play Photo & Video Permissions review (the grid mode is opt-in) - ๐จ Deep customization โ
RelaxPickerThemeexposes colors, text/button styles, icons, labels, and full widget-slot builders - โก Lightweight โ no in-app library scanning; the OS returns only what the user picks
Screenshots #
| Default theme | Custom theme + builders |
|---|---|
![]() |
![]() |
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
relax_image_picker: ^3.1.0
Then run:
flutter pub get
Gallery modes #
The picker offers two ways to browse photos/videos, selected with galleryMode:
| Mode | UX | Permissions | Play policy |
|---|---|---|---|
RelaxGalleryMode.systemPicker (default) |
Opens the OS photo picker (Android Photo Picker / iOS PHPickerViewController) |
None | Not subject to the Photo & Video Permissions policy |
RelaxGalleryMode.inAppGrid |
Renders the WhatsApp-style grid inside the sheet (album selector, multi-select, "Selected photos" banner) | READ_MEDIA_IMAGES / READ_MEDIA_VIDEO (+ iOS photo-library string) |
Requires a completed Play Photo & Video Permissions declaration |
// Permission-free (recommended for occasional attach):
RelaxImagePicker.pick(context); // systemPicker is the default
// In-app grid (browsing the whole library is a core feature):
RelaxImagePicker.pick(context, galleryMode: RelaxGalleryMode.inAppGrid);
Choosing a mode. Use
systemPickerunless browsing the entire library is a core feature of your app.inAppGridreintroducesREAD_MEDIA_*, so Google Play will require you to justify it in the Photo and Video Permissions declaration (and reject apps that only need occasional access).
Least-privilege in inAppGrid mode #
The grid requests READ_MEDIA_* lazily โ only when its gallery view actually
loads, never at app launch or when the picker first opens. That keeps the
prompt tied to the user deliberately opening the gallery, which is exactly what
Google Play looks for. The recommended flow for a messaging-style app:
Open conversation โ no permission requested
Tap ๐ (attach) โ sheet shows Camera ยท Documents ยท Gallery
Tap "Gallery" โ READ_MEDIA_* requested here, then the grid loads
Because both modes ship in the package, you can also offer the system picker for quick one-off attachment and reserve the in-app grid for users who want to browse their whole library โ honoring least-privilege either way. The grid also supports Android 14+/iOS "Selected photos" (partial) access via the built-in Manage banner, so users can grant a subset instead of the whole library.
Before publishing, re-read Google's current Photo and video permissions policy and make sure your Play Console declaration matches how your app actually uses the library.
Selection gestures #
The grid opens as a plain gallery: no selection bubbles are drawn. Selection starts on a long press, which turns the bubbles on for every tile โ from then on a tap toggles selection, and a long press opens the preview. Deselecting the last item hides the bubbles again.
| Gesture | Bubbles hidden (default) | Bubbles shown (selection mode) |
|---|---|---|
| Tap | Full-screen preview of that item | Select / deselect it |
| Long press | Selects it โ turns the bubbles on | Full-screen preview |
With enablePreview: false there is nothing to preview, so a tap selects
directly (and reveals the bubbles).
Writing your own tile with assetTileBuilder? It receives selectionMode โ
draw the bubble only when it is true.
The camera page #
Whatever cameraFirst is set to, the camera is never a bare viewfinder: the
gallery follows it as a horizontal strip of small squares pinned at the bottom,
so the user can keep picking photos while shooting. Dragging the strip up (or
tapping its handle) grows it into the very same grid the default layout shows โ
camera tile, album selector, limited-access banner and all. Dragging back down,
tapping the camera tile inside the grid, or the back gesture returns to the
camera.
cameraFirst only decides where the picker starts:
RelaxImagePicker.pick(
context,
galleryMode: RelaxGalleryMode.inAppGrid, // required
cameraFirst: true, // defaults to false
);
false(default) โ the picker opens on the grid sheet; tapping the camera tile opens the camera page above it, and closing the camera comes back to the sheet.trueโ the picker opens directly on the camera page; closing it closes the picker.
Notes:
cameraFirstneedsgalleryMode: RelaxGalleryMode.inAppGridandenableCamera: true; it is ignored otherwise.- Captures are appended to the current selection instead of closing the picker, and lead both the strip and the grid so a fresh shot is always visible.
- A video recording can be held and resumed โ the control sits next to the capture button while recording, both halves land in the same file, and the counter only runs while frames do. Devices that don't support it keep recording (the failure is logged, not surfaced).
- The documents view is not part of the camera page โ use the sheet's Documents tab for those.
The preview step #
Unless enablePreview: false, items open full screen for review: swipe between
them, toggle the selection from the corner check, and confirm with the send
button. Selection changes are reported live, so the sheet underneath stays in
sync whatever the user does here.
Videos play inline โ camera captures and library picks alike. Tap anywhere to play or pause, scrub with the progress bar, and tap again once it ends to replay from the start. Only the page on screen ever plays: swiping to the next item pauses the previous one instead of leaving audio running off-screen.
Images support pinch-to-zoom, and documents render their thumbnail (first PDF page, image preview, or a type icon).
Platform setup #
The gallery needs no media-storage permission. Browsing is delegated to the OS photo picker and documents to the Storage Access Framework, so the only permissions you ever declare are for the optional in-app camera.
Android
Gallery browsing goes through the Android Photo Picker and documents through the
Storage Access Framework, so you never declare READ_MEDIA_IMAGES,
READ_MEDIA_VIDEO or READ_EXTERNAL_STORAGE. Only the in-app camera
(enableCamera) needs permissions, in
android/app/src/main/AndroidManifest.xml:
<!-- Camera capture (only when enableCamera: true) -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Recording video *with sound* via the in-picker camera -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
With
enableCamera: false, the picker needs zero manifest permissions. The package also never requestsMANAGE_EXTERNAL_STORAGE.
Using RelaxGalleryMode.inAppGrid? The in-app grid reads the library, so add
the granular media permissions (and complete the Play declaration โ see
Gallery modes):
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android 14+ partial ("Selected photos") access -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!-- Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
Legacy
READ_EXTERNAL_STORAGE. Thecameradependency declaresWRITE_EXTERNAL_STORAGE(maxSdkVersion="28"), which makes Android's manifest merger auto-add an unscoped legacyREAD_EXTERNAL_STORAGE. It is inert on Android 13+ and does not fall under the Photo & Video Permissions policy, but you can scope it out of modern Android by adding this to your app manifest (withxmlns:tools="http://schemas.android.com/tools"on<manifest>):<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:node="replace" />
iOS
Gallery picking uses the system photo picker (PHPickerViewController), which
needs no NSPhotoLibraryUsageDescription. Only add usage strings for the
in-app camera in ios/Runner/Info.plist, and make them specific โ Apple
frequently rejects vague purpose strings:
<!-- Camera capture (enableCamera) -->
<key>NSCameraUsageDescription</key>
<string>Lets you take a photo or record a video to send.</string>
<!-- Recording video with sound -->
<key>NSMicrophoneUsageDescription</key>
<string>Records sound when you capture a video.</string>
Minimal permission sets #
Add only the lines for the features you enable:
| Feature you use | Android | iOS |
|---|---|---|
Gallery (allowImages / allowVideos) |
(none โ OS photo picker) | (none โ system photo picker) |
Documents (allowDocuments) |
(none โ uses SAF) | (none โ uses the system file picker) |
Camera photo (enableCamera) |
CAMERA |
NSCameraUsageDescription |
| Camera video with sound | CAMERA, RECORD_AUDIO |
NSCameraUsageDescription, NSMicrophoneUsageDescription |
Store review โ no media-permission gate #
Because gallery browsing uses the OS photo picker, the app declares no
READ_MEDIA_IMAGES / READ_MEDIA_VIDEO, so Google Play's Photo and Video
Permissions policy declaration does not apply โ the single most common
media-picker rejection is removed entirely. The OS picker also gives the user
least-privilege, per-pick access with no in-app library scanning.
- Apple App Store. The camera
*UsageDescriptionstrings are mandatory whenenableCamerais on โ without them the app crashes on access. The photo library string is not required for the system picker. - Data safety / privacy. Still declare any camera access in the Play Data safety form and your App Store privacy details.
Usage #
Basic usage #
import 'package:relax_image_picker/relax_image_picker.dart';
final result = await RelaxImagePicker.pick(context);
print('Total files: ${result.files.length}');
print('Images: ${result.images.length}');
print('Videos: ${result.videos.length}');
print('Documents: ${result.documents.length}');
for (final file in result.files) {
print('File: ${file.path} ยท ${file.size} bytes');
}
pick always returns a RelaxPickerResult. When the user cancels or permissions
are denied, the result is empty (result.isEmpty == true).
Advanced configuration #
final result = await RelaxImagePicker.pick(
context,
allowImages: true,
allowVideos: true,
allowDocuments: true,
enableCamera: true,
enablePreview: true,
maxSelection: 30,
enableCompression: false,
cameraFirst: false,
acceptedDocumentTypes: ['pdf', 'doc', 'docx'],
accentColor: const Color(0xFF25D366),
title: 'Select media',
);
Theming #
Pass a RelaxPickerTheme to override colors, text and button styles, icons, and
labels. Every style field is nullable and falls back to a sensible default, so an
empty RelaxPickerTheme() reproduces the default look.
Labels are French by default (noMediaLabel, browseLabel, pauseRecordingLabel,
โฆ) โ override them to localize the picker.
final result = await RelaxImagePicker.pick(
context,
theme: RelaxPickerTheme(
accentColor: const Color(0xFF6C4DF6),
sheetBorderRadius: 32,
tileBorderRadius: 18,
titleTextStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800),
maxSelectionLabelBuilder: (max) => 'You can pick at most $max',
),
);
Widget-slot builders #
For full control, RelaxPickerTheme exposes builders that replace individual
widgets entirely. Any builder left null falls back to the default themed widget.
| Builder | Replaces |
|---|---|
sendButtonBuilder ยท cancelButtonBuilder ยท confirmButtonBuilder ยท browseButtonBuilder |
The action buttons |
bottomBarBuilder |
The whole bottom action bar |
tabBuilder |
A gallery/documents tab |
assetTileBuilder |
A grid tile in inAppGrid mode โ receives selectionMode, so hide your bubble while it is false |
mediaTileBuilder ยท documentTileBuilder |
A tray tile / a document tile |
cameraTileBuilder |
The camera tile at the head of the grid |
captureButtonBuilder |
The photo / video capture buttons |
pauseButtonBuilder |
The hold/resume control shown while recording |
emptyMediaBuilder ยท emptyDocumentsBuilder |
The empty-state placeholders |
RelaxPickerTheme(
accentColor: accent,
sendButtonBuilder: (context, {required selectedCount, required processing, required onSend}) {
return FilledButton(
onPressed: onSend,
child: processing
? const CircularProgressIndicator(strokeWidth: 2)
: Text('Send ($selectedCount)'),
);
},
);
See the example/ app for a complete demonstration mixing style
overrides and widget builders.
API reference #
RelaxImagePicker.pick() #
Opens the media picker with the given configuration and returns the selection.
| Parameter | Type | Default | Description |
|---|---|---|---|
context |
BuildContext |
required | Build context used to show the sheet |
allowImages |
bool |
true |
Enable image selection |
allowVideos |
bool |
true |
Enable video selection |
allowDocuments |
bool |
true |
Enable document selection |
enableCamera |
bool |
true |
Show the in-picker camera |
enablePreview |
bool |
true |
Enable the full-screen preview step |
maxSelection |
int |
30 |
Maximum number of items selectable |
enableCompression |
bool |
false |
Compress images on selection |
cameraFirst |
bool |
false |
Open on the live camera with a draggable gallery strip (needs inAppGrid + enableCamera) |
galleryMode |
RelaxGalleryMode |
systemPicker |
systemPicker (OS picker, no permission) or inAppGrid (in-app grid, needs READ_MEDIA_*) |
acceptedDocumentTypes |
List<String>? |
null |
Allowed document extensions |
accentColor |
Color |
0xFF25D366 |
Accent color when no theme is given |
theme |
RelaxPickerTheme? |
null |
Full UI customization |
title |
String |
'Select media' |
Sheet title |
confirmButtonText / cancelButtonText / validateButtonText |
String |
โ | Action labels |
galleryTabText / cameraTabText / documentsTabText |
String |
โ | Tab labels |
Returns: Future<RelaxPickerResult>
RelaxPickerResult #
All selected media organized by type.
| Property | Type | Description |
|---|---|---|
files |
List<RelaxMediaFile> |
All selected files |
images |
List<RelaxImageFile> |
Selected images only |
videos |
List<RelaxVideoFile> |
Selected videos only |
documents |
List<RelaxDocumentFile> |
Selected documents only |
isEmpty |
bool |
true when nothing was selected |
hasMedia |
bool |
true when at least one file was selected |
Media file models #
RelaxMediaFile (base) โ id, path, mimeType, size, thumbnailPath?, creationDate?
RelaxImageFileaddswidth,height,albumId?RelaxVideoFileaddsduration,width,height,isMuted,albumId?RelaxDocumentFileaddsfileName,extension,canPreview(plustoJson/fromJsonfor caching)
Metadata note. The OS photo picker returns files, not library metadata, so in
systemPickermode imagewidth/heightare derived on the fly, picked videos carry noduration/dimensions (they default toDuration.zero/0), andalbumIdis alwaysnull.
thumbnailPath. Set for videos picked from the library ininAppGridmode: a JPEG still exported to the cache directory, so you can render a preview without decoding the movie file. It staysnullfor images (theirpathalready is the picture) and for camera captures / OS-picker results โ there is no library thumbnail to export, and the package doesn't decode video frames itself.
Platform support #
| Platform | Supported | Notes |
|---|---|---|
| Android | โ | Gallery via the Android Photo Picker (ACTION_PICK_IMAGES, SAF fallback โค API 32) โ no media permission |
| iOS | โ | Gallery via the system photo picker (PHPickerViewController) โ no photo-library permission |
Architecture #
lib/src/
โโโ controllers/ # Business logic and state management
โโโ models/ # Data models, result objects, theme & builders
โโโ services/ # Platform integrations (photo_manager, camera, file_picker)
โโโ widgets/ # UI components (gallery, camera, document pickers, preview)
โโโ relax_image_picker.dart # Public API
Contributing #
Issues and pull requests are welcome in the relax-tech monorepo.
License #
This project is licensed under the MIT License โ see the LICENSE file for details.


