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 files
  • Built-in image compression with adjustable quality
  • File extension filtering for the file picker
  • Streaming reads of any picked file, chunk by chunk, without loading it into memory
  • Handles Android activity recreation (returns lost results via getLostFiles)
  • Copies a file only when it has to: compression is the single reason the plugin ever writes to the cache, everything else is read from where the user keeps it

Requirements

Platform Minimum version
Android minSdk 24, host activity must be a ComponentActivity
iOS 13.0 (CocoaPods) / 15.0 (Swift Package Manager)

Installation

Add this to your pubspec.yaml:

dependencies:
  plux_media_picker: ^1.0.3

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.

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 use CameraMediaType.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: CameraMediaType.image, quality: 0.8);

// Record a video
PluxFile? video = await picker.pickCamera(mediaType: CameraMediaType.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.

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

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.

Pick from File System

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

The document is never copied, so PluxFile.path is empty and PluxFile.uri is the only handle — read it with readFileStream.

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.

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. If the native side cannot open the uri, the call throws PluxMediaPickerExceptionCode.fileStreamCreationFailed.

uri and path

PluxFile.uri is always set and is what readFileStream needs. PluxFile.path is set only when the plugin actually created a file, so it is empty exactly where nothing was copied:

Source uri path
pickCamera always always (the capture is the plugin's own file)
pickGallery always only for re-encoded images on Android; always on iOS
pickFile always never

When path is empty, dart:io cannot open the file — on Android the uri belongs to another app, and on iOS it is security scoped. Use readFileStream instead.

How long a pickFile uri stays readable differs by platform: Android takes a persistable grant, so it survives a restart until clearCache releases it, while on iOS the scoped url is valid for the current run of the app only. Do not store it as a long lived reference on iOS.

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 uri and can be streamed
    }
  });
}

A recovered result is reported once and then forgotten. 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. Any PluxFile.path from an earlier pick becomes invalid afterwards. On Android the call also releases the access granted to documents picked with pickFile, so uris returned earlier stop working.

Return Values & Error Handling

  • pickCamera returns null if the user cancels.
  • pickGallery returns an empty list [] if nothing is selected.
  • pickFile returns null if nothing is selected.
  • 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 stream cannot be created, and forwards read errors to the stream's onError.
try {
  final file = await picker.pickCamera();
} on PluxMediaPickerException catch (ex) {
  debugPrint('${ex.code}: ${ex.description}');
}

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

API Reference

Method Parameters Returns Description
pickCamera mediaType: CameraMediaType (default image), quality: double (0.8) Future<PluxFile?> Opens the camera for a single media file.
pickGallery maxLimit: int (default 10), quality: double (default 0.8) Future<List<PluxFile>> Opens the gallery for media selection.
pickFile allowedExtensions: List<String> (default []) Future<PluxFile?> Opens the file manager.
readFileStream uri: String, bufferSize: int (default 256 KB) Future<Stream<Uint8List>> Reads the file behind a PluxFile.uri in chunks.
getLostFiles Future<List<PluxFile>> Retrieves files selected before activity recreation (Android).
clearCache Future<bool> Clears the files the plugin created.

PluxFile

Field Description
uri Platform uri, always set; the handle readFileStream expects
path Absolute file path, set only when the plugin created a file
name File name with extension
size Size in bytes

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, streamed reads with a chunk size selector, progress and cancellation, lost-file recovery and cache clearing.

Libraries

plux_media_picker