plux_media_picker 2.0.0 copy "plux_media_picker: ^2.0.0" to clipboard
plux_media_picker: ^2.0.0 copied to clipboard

A Flutter plugin that provides a unified API to pick photos, videos, and files from the device's gallery, camera, and file system.

plux_media_picker #

A Flutter plugin that provides a unified API to pick photos, videos, and files from the device's gallery, camera, and file system. Supports iOS and Android only.

Features #

  • Uses native system pickers (no custom UI)
  • Single or multiple selection of media and of documents
  • Built-in image compression with adjustable quality
  • Media type filter for the gallery, extension filter for the file picker
  • Every result reports its mime type and what kind of content it is
  • Streaming reads of any picked file, chunk by chunk, without loading it into memory
  • Native copying, so a file that has to end up on disk never travels through Dart
  • Handles Android activity recreation (returns lost results via getLostFiles)
  • Copies a file only when it has to, and says exactly when in What every call does to the file

Requirements #

Platform Minimum version
Android minSdk 24, host activity must be a ComponentActivity
iOS 15.0

Installation #

Add this to your pubspec.yaml:

dependencies:
  plux_media_picker: ^2.0.0

Setup #

Android #

The camera and gallery pickers use registerForActivityResult, which requires the host activity to extend ComponentActivity. The default FlutterActivity does not, so change MainActivity to FlutterFragmentActivity:

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

Without this the plugin throws IllegalStateException when the activity attaches.

Only the camera needs a permission:

  <uses-permission android:name="android.permission.CAMERA" />

The plugin requests it at runtime and reports a denial as PluxMediaPickerExceptionCode.permissionAccessDenied. Picking from the gallery goes through the Android photo picker, which runs in a separate process and grants access to the chosen items only, so no storage permission is required. The FileProvider used for camera captures is declared by the plugin's own manifest under the authority ${applicationId}.pluxmediapicker.fileprovider, which deliberately differs from the one image_picker_android claims - two providers sharing an authority break the install.

iOS #

Add the following keys to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access to record video</string>

NSMicrophoneUsageDescription is required only if you record video. NSPhotoLibraryUsageDescription is not needed: the gallery is shown by PHPickerViewController, which runs out of process and returns only what the user picked.

Usage #

First, create an instance of the plugin:

import 'package:plux_media_picker/plux_media_picker.dart';

final picker = PluxMediaPicker();

Pick from Camera #

// Take a photo
PluxFile? photo = await picker.pickCamera(mediaType: PluxMediaType.image, quality: 0.8);

// Record a video
PluxFile? video = await picker.pickCamera(mediaType: PluxMediaType.video, quality: 0.8);

quality is the JPEG quality in the 0.0 – 1.0 range and applies to images only — a recording is handed over exactly as the camera produced it. On Android a still at quality: 1.0 is not re-encoded either; on iOS the controller hands over a decoded UIImage, so a still always goes through the JPEG encoder and quality only decides how hard it squeezes.

List<PluxFile> result = await picker.pickGallery(maxLimit: 10, quality: 0.8);

// Only photos, or only videos:
List<PluxFile> photos = await picker.pickGallery(mediaType: PluxMediaType.image);

// Guarantee a PluxFile.path, for a preview widget or another plugin:
List<PluxFile> files = await picker.pickGallery(asFile: true);

By default an image is copied only when it has to be re-encoded, that is when quality < 1.0; videos and full quality images keep their original location and are read with readFileStream.

Pass asFile: true when the result has to be a real file anyway. The copy then happens inside the pick, instead of a copyToFile call per item afterwards.

Pick from File System #

// Pass extensions without a leading dot; an empty list allows any file type.
List<PluxFile> result = await picker.pickFiles(allowedExtensions: ['pdf', 'txt']);

// Several documents at once:
List<PluxFile> many = await picker.pickFiles(maxLimit: 5);

Documents are never copied, so PluxFile.path is null and PluxFile.uri is the only handle — read it with readFileStream, or materialise it with copyToFile.

Extensions are mapped to MIME types on Android and to UTTypes on iOS; ones that cannot be resolved are logged and skipped, and if none of them resolve the picker shows all files.

uri and path #

PluxFile.uri is an opaque handle: always set, always what readFileStream expects, and valid until clearCache. What it holds differs by platform, and the caller is not meant to care.

On Android it is the platform uri itself - a photo picker or SAF content://, or a file:// for what the plugin wrote. It is readable as is, and pickFiles takes a persistable grant so it keeps working after a restart.

On iOS it is a plux:// token backed by a bookmark. A url vended by UIDocumentPickerViewController carries a sandbox extension that belongs to that url instance: hand it over as a string and the rebuilt url can no longer enter the security scope, which is what makes an iCloud Drive document unreadable. A bookmark taken while the scope is open keeps the access, and it also survives a relaunch, which a scoped url does not.

PluxFile.path is set only when a real file exists that dart:io may open, so it is null exactly where nothing was copied.

What every call does to the file #

"Copy" below means the plugin writes the whole content somewhere new. A move is not a copy: it only renames the entry, and it falls back to a copy across volumes. A re-encode produces new bytes by definition, so it is counted separately.

Call Platform Case What the plugin does path Copy
pickCamera(image, quality < 1.0) Android the camera writes into the plugin cache, the still is re-encoded in place set re-encode, one file
pickCamera(image, quality = 1.0) Android the camera writes into the plugin cache set no
pickCamera(video) Android the camera writes into the plugin cache set no
pickCamera(image) iOS the controller hands over a UIImage, so it is always encoded to JPEG in the plugin cache set one write, no original file exists
pickCamera(video) iOS the recording is moved into the plugin cache set move
pickGallery(asFile: false) Android video, or image at quality = 1.0 nothing, the photo picker uri is returned as is null no
pickGallery(asFile: false) Android image at quality < 1.0 re-encoded into the plugin cache set re-encode
pickGallery(asFile: true) Android video, or image at quality = 1.0 copied into the plugin cache set yes
pickGallery(asFile: true) Android image at quality < 1.0 re-encoded into the plugin cache, nothing extra set re-encode
pickGallery(...) iOS image at quality < 1.0 the item provider file is adopted, then re-encoded set move + re-encode
pickGallery(...) iOS everything else the item provider file is adopted into the plugin cache set move
pickFiles(...) both nothing, the document stays where the user keeps it null no
readFileStream(uri) both opens a native reader and emits chunks no
copyToFile(file, path) both copies the content natively to path set on the returned file yes
getLostFiles() Android camera result the capture is already the plugin's own file set no
getLostFiles() Android gallery result copied into the plugin cache at pick time, because the grant dies with the process set yes
getLostFiles() iOS always empty no
clearCache() both deletes the plugin's own files, forgets the handles, releases the document grants

asFile on iOS changes nothing: a gallery item is always adopted into the cache, because the photo library deletes its own copy as soon as the pick is delivered.

So the plugin writes new bytes in exactly three situations: an image is re-encoded, asFile or copyToFile asks for a file that does not exist yet, and a gallery pick has to be kept for getLostFiles. Everything else is either a move or a plain read of the original.

Reading a file as a stream #

readFileStream opens a native reader for a PluxFile.uri and emits the content in chunks, which keeps large files off the Dart heap:

final stream = await picker.readFileStream(result.uri);

var received = 0;
await for (final Uint8List chunk in stream) {
  received += chunk.length;
  debugPrint('$received / ${result.size}');
}

Every PluxFile this plugin returns can be read this way, including recovered lost files. Reading always starts at the beginning of the file.

Cancelling the subscription stops the native reader as well:

final subscription = stream.listen(handleChunk);
await subscription.cancel();

bufferSize sets the chunk size (256 KB by default). Larger chunks mean fewer platform messages and more memory per chunk:

final stream = await picker.readFileStream(result.uri, bufferSize: 4 * 1024 * 1024);

Each call creates its own stream, so several files can be read at the same time, and the channel is torn down as soon as the stream ends or is cancelled. A stream that is opened and never listened to is dropped after a minute. If the handle cannot be resolved, the call throws PluxMediaPickerExceptionCode.fileStreamCreationFailed; a failure to read the bytes is reported on the stream's onError.

Copying a file #

When something else needs a real file — another plugin, a converter, a preview widget — let the platform copy it:

final copy = await picker.copyToFile(result, '${directory.path}/${result.name}');
final file = File(copy.path!);

copyToFile never moves the bytes through Dart. Streaming a 200 MB video only to write it back to disk would push every byte across the platform channel in ~800 messages; this does the same work in one native pass. Use readFileStream when the content is only passed through, and copyToFile when a File is genuinely required.

Recovering Lost Results (Android only) #

If your app is killed by the system while the picker is open, the plugin saves the selected result. To retrieve it after the activity is recreated, call getLostFiles() (e.g., in initState):

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    List<PluxFile> lostFiles = await picker.getLostFiles();
    if (lostFiles.isNotEmpty) {
      // handle the recovered files - they carry a handle and can be streamed or copied
    }
  });
}

A recovered result is reported once and then forgotten, and each file remembers which picker produced it in PluxFile.source, so it can be handed back to the call it belongs to. Gallery picks are copied into the plugin's cache before being stored, because a photo picker grant does not outlive the process. On iOS this always returns an empty list.

Clearing the cache #

The plugin writes compressed images and camera captures into its own directory inside the app cache. Once you have copied what you need, drop them:

await picker.clearCache();

Only the plugin's own files are removed; the rest of the app cache is left alone. It also gives back the document grants it took and, on iOS, forgets every handle, so a PluxFile.uri from an earlier pick stops working — call it when nothing picked so far is still in use, typically at app start.

Return Values & Error Handling #

  • pickCamera returns null if the user cancels.
  • pickGallery and pickFiles return an empty list [] if nothing was selected. A cancellation is deliberately reported the same way, since there is nothing to handle differently.
  • All three throw PluxMediaPickerException on failure (denied permission, compression or save error); wrap calls in try-catch.
  • Calling a picker while another call of the same kind is pending fails the new call instead of abandoning the previous one.
  • readFileStream throws PluxMediaPickerException if the handle cannot be resolved, and forwards read errors to the stream's onError.
try {
  final file = await picker.pickCamera();
} on PluxMediaPickerException catch (ex) {
  debugPrint('$ex');
}

PluxMediaPickerExceptionCode values: loadFileFailed, invalidImageData, compressionFailed, saveFailed, invalidFile, permissionAccessDenied, mediaTypeGetFailed, fileStreamCreationFailed.

API Reference #

Method Parameters Returns Description
pickCamera mediaType: PluxMediaType (default image), quality: double (0.8) Future<PluxFile?> Opens the camera for a single media file.
pickGallery maxLimit: int (10), quality: double (0.8), mediaType: PluxMediaType?, asFile: bool (false) Future<List<PluxFile>> Opens the gallery for media selection.
pickFiles allowedExtensions: List<String> ([]), maxLimit: int (1) Future<List<PluxFile>> Opens the file manager.
readFileStream uri: String, bufferSize: int (256 KB) Future<Stream<Uint8List>> Reads the file behind a PluxFile.uri in chunks.
copyToFile file: PluxFile, destinationPath: String Future<PluxFile> Copies the content natively and describes the copy.
getLostFiles Future<List<PluxFile>> Retrieves files selected before activity recreation (Android).
clearCache Future<bool> Clears the files the plugin created and forgets every handle.

pickCamera and pickGallery reject PluxMediaType.file with an ArgumentError: documents are what pickFiles is for.

PluxFile #

Field Description
uri Opaque handle, always set; what readFileStream and copyToFile expect
path Absolute file path, null when the plugin created no file
name File name with extension
size Size in bytes, null when the source does not report one
mimeType Reported by the platform, not guessed from the name
type image, video or file
source Which picker produced it: camera, gallery or files

Example #

The app in example/ exercises every method: camera photo and video with a quality slider, gallery multi-select with a limit, file picking with an extension filter, native copying, streamed reads with a chunk size selector, progress and cancellation, lost-file recovery and cache clearing.

0
likes
160
points
16
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin that provides a unified API to pick photos, videos, and files from the device's gallery, camera, and file system.

Repository (GitHub)
View/report issues

Topics

#media #image-picker #file-picker #camera #gallery

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on plux_media_picker

Packages that implement plux_media_picker