relax_image_picker 2.1.0 copy "relax_image_picker: ^2.1.0" to clipboard
relax_image_picker: ^2.1.0 copied to clipboard

A powerful, WhatsApp-like media picker for Flutter

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
  • ๐Ÿ“„ Document selection โ€” pick files from device storage, with recent-documents recall between sessions
  • ๐Ÿ‘๏ธ Full-screen preview โ€” review images, videos, and documents 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: ^2.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.

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,
  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.

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 let you replace individual widgets entirely (send button, tabs, media/document tiles, empty states, the bottom bar, the capture button, and more). Any builder left null falls back to the default themed widget.

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
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. Image width/height are derived on the fly; gallery-picked videos carry no duration/dimensions (they default to Duration.zero / 0). albumId is always null for gallery picks.

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.