Pick
Open the system gallery, camera, file dialog and save dialog through the Pick facade and get a MagicFile back on every platform.
On this page
Basic Usage#
import 'package:magic_picker/magic_picker.dart';
Future<void> main() async {
final image = await Pick.image(maxWidth: 1024, imageQuality: 85);
if (image == null) return; // The user cancelled.
print('${image.name}: ${image.size} bytes');
await image.store('avatars/profile.jpg');
}
Pick is a static facade over image_picker and file_picker. Every picker that returns a file answers with a MagicFile from core, or null (a list: empty) when the user cancels, so a cancelled pick is never an error.
Quick Reference#
| Member | Returns | Meaning |
|---|---|---|
Pick.image({maxWidth, maxHeight, imageQuality}) |
Future<MagicFile?> |
One image from the gallery. imageQuality is 0 to 100. |
Pick.images({maxWidth, maxHeight, imageQuality}) |
Future<List<MagicFile>> |
Several images from the gallery. |
Pick.camera({preferredCamera, maxWidth, maxHeight, imageQuality, fallbackToGallery, onError}) |
Future<MagicFile?> |
A photo from the camera. |
Pick.media({maxWidth, maxHeight, imageQuality}) |
Future<MagicFile?> |
One image or one video from the gallery. |
Pick.video({maxDuration}) |
Future<MagicFile?> |
One video from the gallery. |
Pick.recordVideo({preferredCamera, maxDuration, fallbackToGallery, onError}) |
Future<MagicFile?> |
A video recorded with the camera. |
Pick.file({extensions}) |
Future<MagicFile?> |
One file of any type, or of the listed extensions. |
Pick.files({extensions}) |
Future<List<MagicFile>> |
Several files. |
Pick.directory() |
Future<String?> |
A directory path. Not supported on the web. |
Pick.saveFile({fileName, bytes, mimeType, dialogTitle}) |
Future<Uri?> |
Writes bytes through the save dialog and answers where they went. |
Images#
import 'package:magic_picker/magic_picker.dart';
Future<void> main() async {
final image = await Pick.image(maxWidth: 800, maxHeight: 800);
final gallery = await Pick.images(imageQuality: 80);
print(image?.name);
print(gallery.length); // 0 when the user cancels
}
maxWidth and maxHeight are pixel limits that shrink the picked image; leave them unset to keep the original. imageQuality is a number from 0 to 100.
Camera#
import 'package:magic_picker/magic_picker.dart';
Future<void> main() async {
final photo = await Pick.camera(
preferredCamera: CameraDevice.front,
fallbackToGallery: true,
onError: (Object error) => print('Camera failed: $error'),
);
print(photo?.name);
}
CameraDevice is re-exported from this package, so no extra import is needed. When the camera fails, for example because the permission was refused, the outcome depends on two arguments:
onError |
fallbackToGallery |
What happens on failure |
|---|---|---|
| unset | false |
The error is rethrown. |
| set | false |
onError runs and the call answers null. |
| either | true |
onError runs when set, then the gallery picker opens. |
With fallbackToGallery: true the gallery also opens when the user closes the camera without a photo. Pick.recordVideo takes the same two arguments and falls back to Pick.video.
Video#
import 'package:magic_picker/magic_picker.dart';
Future<void> main() async {
final clip = await Pick.video(maxDuration: const Duration(seconds: 30));
final recording = await Pick.recordVideo(
preferredCamera: CameraDevice.rear,
maxDuration: const Duration(minutes: 1),
);
print(clip?.isVideo); // true
print(recording?.name);
}
Files#
import 'package:magic_picker/magic_picker.dart';
Future<void> main() async {
final anyFile = await Pick.file();
final pdf = await Pick.file(extensions: ['pdf']);
final documents = await Pick.files(extensions: ['pdf', 'doc', 'docx']);
final directory = await Pick.directory();
print([anyFile?.name, pdf?.name, documents.length, directory]);
}
extensions are written without the dot. Passing the list restricts the dialog to those types; leaving it out allows any file. File contents are not read at pick time: bytes load on demand through readAsBytes, so a large selection costs nothing until you store or upload it.
Saving a File#
import 'dart:typed_data';
import 'package:magic_picker/magic_picker.dart';
Future<void> save(Uint8List pdfBytes) async {
final savedTo = await Pick.saveFile(
fileName: 'report.pdf',
bytes: pdfBytes,
);
if (savedTo != null && savedTo.scheme == 'file') {
print(savedTo.toFilePath());
}
}
saveFile opens the platform save dialog, writes the bytes and answers the Uri it wrote to, or null when the user cancels. The web has no dialog here: the browser downloads the file. The scheme depends on the platform:
| Platform | Scheme |
|---|---|
| Desktop and iOS | file |
| Android | None: the answer is the path part of the Storage Access Framework document uri, not a path you can open |
| Web | None: the answer is always null, because the browser reports no location |
Call toFilePath() only after checking for file. The MIME type comes from the extension of fileName (report.pdf becomes application/pdf) and falls back to application/octet-stream. Pass mimeType when the extension does not describe the content: Android and the browser both use it to choose the app that opens the file.
Working with the Result#
MagicFile is core's class; these are the members used most after a pick.
| Member | Meaning |
|---|---|
name, extension, mimeType |
The file name, its extension without the dot, and a MIME type: the one the picker reports, else one derived from the name (null when the extension is unknown). |
size |
Bytes, filled for every pick. Treat null and 0 as unknown when you enforce an upload limit. |
path |
The file path when the platform gives one, null otherwise; mobile and desktop picks give one. On the web an image, video or media pick answers a blob: URL; a Pick.file or Pick.files pick answers null. |
isImage, isVideo |
Derived from the extension. |
readAsBytes() |
The bytes, read once and cached. |
store(path, {disk}), storeAs(directory, {disk}) |
Write to Storage; storeAs generates a unique name. |
upload(url, {fieldName, data, headers}) |
Send as multipart form data through Http. |
import 'package:magic_picker/magic_picker.dart';
Future<void> uploadAvatar() async {
final image = await Pick.image(maxWidth: 512, imageQuality: 80);
if (image == null) return;
final response = await image.upload('/api/user/avatar', fieldName: 'avatar');
print(response.successful);
}
Platform Support#
| Method | Android | iOS | Web | Desktop |
|---|---|---|---|---|
image, images, media, video |
yes | yes | yes | yes |
camera, recordVideo |
yes | yes | yes | needs a custom image_picker camera delegate |
file, files, saveFile |
yes | yes | yes | yes |
directory |
yes | yes | no | yes |
Each platform's permission setup (photo library and camera usage descriptions on iOS and macOS, file-access entitlements on macOS) is the app's responsibility.
Gotchas#
| Mistake | Fix |
|---|---|
Treating null as a failure |
null is a cancelled pick. Return early. |
Calling Pick.camera with no onError and no fallback |
A refused permission rethrows. Pass one of the two. |
Reading path on the web |
It is a blob: URL for an image pick and null for a Pick.file pick, and neither is a file path; use readAsBytes or store. |
Trusting size as a limit |
It can be null or 0 when the file vanished after the pick. Check the stored size too. |
Calling savedTo.toFilePath() without checking the scheme |
Only a file uri is a path you can open; the Android answer has no scheme, so it converts to a string that is not a real file path, and the web answers null. |
Passing withData to Pick.file or Pick.files |
The parameter no longer exists. Read bytes with readAsBytes. |
Calling Pick.directory on the web |
It is not supported there. |
Related Documentation#
- XFile Conversion: handing a picked file to code that speaks
XFile. - Download: sharing a stored file with
Storage.download. - Testing: testing code that calls
Pick.