Image Picker in Flutter: Camera, Gallery, and Lost-Data Handling
image_picker is a federated plugin with platform quirks on every OS — plist keys, Android process death, a cache that is not permanent, and a camera API desktop does not support by default.
Published on • October 14, 2026
AI Assistant

“Let the user attach a photo” is one of the most common Flutter features, and image_picker is the standard way to build it — a Flutter-team package on a verified publisher, supporting Android, iOS, Linux, macOS, web, and Windows.
It is also a federated plugin: image_picker_android, image_picker_ios, image_picker_for_web, image_picker_macos, image_picker_linux, image_picker_windows, and a platform interface sit behind the one package name. That architecture is why the API is uniform while the setup is emphatically not.
The API in one screen
Everything returns an XFile from the cross_file package — a small abstraction over a path or bytes that works across platforms.
final picker = ImagePicker();
// Gallery
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
// Camera
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1600,
maxHeight: 1600,
imageQuality: 85,
);
// Multiple images (Android/iOS photo picker; desktop falls back to file selection)
final List<XFile> images = await picker.pickMultiImage();
// Video
final XFile? video = await picker.pickVideo(source: ImageSource.gallery);
// Either kind, one call
final XFile? media = await picker.pickMedia();
maxWidth/maxHeight/imageQuality decode and resize on the platform side before returning — the cheapest place to shrink a 12MP photo before it reaches your Dart isolate. XFile exposes .path, .name, .length(), .readAsBytes(), and .saveTo(...).
Display is a one-liner:
Image.file(File(xfile.path), fit: BoxFit.cover)
Platform setup: the parts that bite
iOS (Info.plist)
Add these keys to ios/Runner/Info.plist:
NSPhotoLibraryUsageDescription— why you need the photo library. (Not requested if you always passrequestFullMetadata: false, but App Store policy still requires the entry.)NSCameraUsageDescription— why you need the camera.NSMicrophoneUsageDescription— only if you record video with sound.
Missing keys do not fail politely — the app terminates when the picker is invoked.
Since 0.8.1 the iOS implementation uses PHPicker on iOS 14+. One known platform issue: picking HEIC images on the iOS simulator fails under PHPicker. Test image selection on a real device.
Android
No manifest configuration is required — scoped storage is handled, and android:requestLegacyExternalStorage is no longer needed. On Android 13+ the plugin uses the system Photo Picker.
Two real-world behaviors to design for:
1. Process death. The picker launches via Intent.ACTION_GET_CONTENT / MediaStore.ACTION_IMAGE_CAPTURE, which moves your app to the background — where Android may kill it under memory pressure. When the intent finishes, Android restarts the activity, and the result never reaches the original call. Recover it at startup:
Future<void> getLostData() async {
final response = await ImagePicker().retrieveLostData();
if (response.isEmpty) return;
final List<XFile>? files = response.files;
if (files != null) {
await handleFiles(files);
} else {
await handleError(response.exception);
}
}
Run this check on every launch. Skipping it is the classic “the photo I just took disappeared” bug.
2. launchMode: singleInstance. Launching the picker from an activity with this launch mode always returns RESULT_CANCELED — the picker runs in a different task and cannot hand the result back. Use singleTask instead.
Web, macOS, Windows, Linux
Desktop implementations wrap file_selector with file-type filters. Selection modifiers (max width/height, quality) are not supported on desktop.
ImageSource.camera has no system UI on desktop, so it is unsupported until you install a cameraDelegate:
class MyCameraDelegate extends ImagePickerCameraDelegate {
@override
Future<XFile?> takePhoto({ImagePickerCameraDelegateOptions? options}) =>
myCameraUi.capture();
}
void main() {
final instance = ImagePickerPlatform.instance;
if (instance is CameraDelegatingImagePickerPlatform) {
instance.cameraDelegate = MyCameraDelegate();
}
runApp(const MyApp());
}
macOS additionally needs the com.apple.security.files.user-selected.read-only entitlement. Web limitations live in image_picker_for_web — notably, no camera capture and no file system path (use readAsBytes()).
The cache is not permanent
Images and videos taken with the camera land in your application’s cache directory — temporary by definition. The OS can clear it at any time; on iOS it can be evicted under pressure.
If the file matters (avatar, upload, attachment), copy it somewhere durable before you return from the picker:
Future<File> persist(XFile picked) async {
final dir = await getApplicationDocumentsDirectory();
final dest = File('${dir.path}/${DateTime.now().millisecondsSinceEpoch}_${picked.name}');
await picked.saveTo(dest.path);
return dest;
}
Then upload from (or reference) the durable copy, and delete it when the upload completes.
Display, upload, and permissions
A complete round trip — pick, show, upload:
Future<void> attachPhoto() async {
final picked = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxWidth: 1600,
imageQuality: 85,
);
if (picked == null) return; // user cancelled — not an error
setState(() => _pending = File(picked.path));
final uri = Uri.parse('https://api.example.com/attachments');
final request = await http.MultipartRequest('POST', uri)
..files.add(await http.MultipartFile.fromPath('file', _pending!.path));
final streamed = await request.send();
if (streamed.statusCode == 201) {
await _pending!.delete(); // clean up the persisted copy
}
}
Permission notes: modern Android uses the photo picker without storage permissions, and iOS grants photo access through the picker flow itself — you generally do not need permission_handler for the picking step. You do need the plist usage descriptions on iOS, and camera permission is requested the first time ImageSource.camera is used.
Errors worth handling explicitly
- Cancellation returns
null. It is not an exception; treat it as a no-op. retrieveLostData()at startup for Android process death.- Platform exceptions (camera in use, no camera present, permission denied) surface as
PlatformException— catch it and show a message rather than letting it reach the crash reporter. - Desktop camera unsupported until a delegate is installed.
- Oversized images — always cap dimensions server-side as well;
maxWidth/imageQualityare client-side hints, and on web they may be ignored.
When to look elsewhere
image_picker is the right default for selecting media. Consider a different package when you need:
- In-app camera capture with live preview, filters, or custom UI →
cameraorcamerawesome. - Cropping/editing after selection →
image_cropper. - Raw bytes without touching the file system on web →
XFile.readAsBytes()may already be enough. - Document scanning or barcode capture → dedicated packages.
Checklist
- Add the three iOS plist keys before shipping; test on a real device (PHPicker/HEIC).
- Call
retrieveLostData()on Android app start. - Persist camera output out of the cache directory before uploading.
- Treat
nullas cancellation,PlatformExceptionas a real error. - Set
maxWidth/imageQualityon every pick. - Install a
cameraDelegateif desktop camera capture matters to you.
Sources: image_picker on pub.dev, image_picker repository.