relax_image_picker 3.1.0 copy "relax_image_picker: ^3.1.0" to clipboard
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: true opens 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 โ€” RelaxPickerTheme exposes 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
Default theme Custom theme

Installation #

Add the dependency to your pubspec.yaml:

dependencies:
  relax_image_picker: ^3.1.0

Then run:

flutter pub get

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 systemPicker unless browsing the entire library is a core feature of your app. inAppGrid reintroduces READ_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:

  • cameraFirst needs galleryMode: RelaxGalleryMode.inAppGrid and enableCamera: 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 requests MANAGE_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. The camera dependency declares WRITE_EXTERNAL_STORAGE (maxSdkVersion="28"), which makes Android's manifest merger auto-add an unscoped legacy READ_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 (with xmlns: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 *UsageDescription strings are mandatory when enableCamera is 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?

  • RelaxImageFile adds width, height, albumId?
  • RelaxVideoFile adds duration, width, height, isMuted, albumId?
  • RelaxDocumentFile adds fileName, extension, canPreview (plus toJson / fromJson for caching)

Metadata note. The OS photo picker returns files, not library metadata, so in systemPicker mode image width/height are derived on the fly, picked videos carry no duration/dimensions (they default to Duration.zero / 0), and albumId is always null.

thumbnailPath. Set for videos picked from the library in inAppGrid mode: a JPEG still exported to the cache directory, so you can render a preview without decoding the movie file. It stays null for images (their path already 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.

3
likes
160
points
235
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues
Contributing

Topics

#image-picker #media #camera #gallery #files

License

MIT (license)

Dependencies

camera, file_picker, flutter, flutter_image_compress, image_picker, path_provider, pdfx, permission_handler, photo_manager, video_player

More

Packages that depend on relax_image_picker