system_media_picker 1.0.1
system_media_picker: ^1.0.1 copied to clipboard
Permission-free image and video picker for Flutter. Wraps the Android System Photo Picker and iOS PHPickerViewController behind a single Dart API — no READ_MEDIA_* / NSPhotoLibrary* declarations needed.
system_media_picker #
A permission-free Flutter plugin for picking multiple images and videos from the user's gallery on Android and iOS. Bridges the Android System Photo Picker and iOS PHPickerViewController behind a single Dart API.
Highlights #
- Zero photo / media / storage permissions on either platform.
- Multiple images or videos per pick (1–20, dynamic).
- Hard cap enforced end-to-end by the system picker UI itself.
- Returns ready-to-use
dart:ioFilepaths in the app's private cache. - Optional
MediaPickerController(ChangeNotifier) withidle/loading/success/errorstate. - No third-party dependencies.
Platform support #
| Platform | Mechanism | Min OS | Permission |
|---|---|---|---|
| Android | MediaStore.ACTION_PICK_IMAGES (or SAF) |
API 21 (minSdk 21) |
None |
| iOS | PHPickerViewController |
iOS 14+ (gated at runtime) | None |
On Android 11/12 (API 30–32) the Photo Picker is delivered through the modular system component shipped via Google Play system updates. On Android 10 and below the plugin transparently falls back to the Storage Access Framework (
ACTION_OPEN_DOCUMENT) — also permission-free.On iOS the plugin's CocoaPod
system_media_picker.podspecdeclares an iOS 13.0 deployment target so host apps don't have to bump theirs;PHPickerViewControlleris gated to iOS 14+ at runtime viaif #available(iOS 14, *).
Installation #
flutter pub add system_media_picker
…or, manually:
dependencies:
system_media_picker: ^1.0.0
That's it. No changes are required to your AndroidManifest.xml,
your iOS Info.plist, or any platform-side initialisation code. The
plugin is auto-registered through Flutter's plugin discovery.
Quick start #
1) Function-style API — SystemMediaPicker #
import 'package:system_media_picker/system_media_picker.dart';
final picker = SystemMediaPicker();
// Image picker (1..20)
final images = await picker.pickImages(limit: 5);
for (final image in images) {
debugPrint('${image.name} → ${image.path} (${image.sizeBytes} bytes)');
}
// Video picker (1..20)
final videos = await picker.pickVideos(limit: 3);
// Probe the device
final hasModernPicker = await picker.isAvailable();
PickedMedia exposes:
| Field | Type | Notes |
|---|---|---|
path |
String |
Absolute path inside the app's cache. |
uri |
String |
Original content:// (Android) / file URL. |
name |
String |
Best-effort display name. |
sizeBytes |
int |
File size in bytes. |
mimeType |
String |
image/jpeg, video/mp4, … |
type |
MediaType |
image, video, or unknown. |
file |
File |
Convenience accessor for dart:io. |
isImage |
bool |
Sugar for type == MediaType.image. |
isVideo |
bool |
Sugar for type == MediaType.video. |
2) UI-friendly API — MediaPickerController #
A ChangeNotifier that owns the current selection, enforces the limit,
and tracks loading / success / error state for you.
class MyPage extends StatefulWidget {
const MyPage({super.key});
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final controller = MediaPickerController(initialLimit: 8);
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Column(
children: [
switch (controller.status) {
PickerStatus.loading =>
const LinearProgressIndicator(),
PickerStatus.error =>
Text(controller.errorMessage ?? 'Unknown error'),
_ => const SizedBox.shrink(),
},
Expanded(child: MyGrid(items: controller.items)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: controller.isLoading || controller.isAtCapacity
? null
: controller.pickImages,
child: const Text('Pick images'),
),
ElevatedButton(
onPressed: controller.isLoading || controller.isAtCapacity
? null
: controller.pickVideos,
child: const Text('Pick videos'),
),
],
),
],
);
},
);
}
}
API surface:
items,imageCount,videoCount,limitstatus,isLoading,isAtCapacity,remainingSlots,errorMessagepickImages(),pickVideos(),pick(MediaType)updateLimit(int),removeAt(int),clear()
Errors #
All failures are subclasses of the sealed MediaPickerException:
| Exception | When it's thrown |
|---|---|
InvalidLimitException |
limit is outside [1, 20]. |
PickerBusyException |
A pick request is already in flight. |
PickerLaunchException |
Native side could not launch the picker. |
PickerUnsupportedOsException |
iOS < 14 (no PHPickerViewController). |
UnsupportedPlatformException |
Host platform is neither Android nor iOS. |
Permissions {#permissions} #
This plugin is the whole reason you can ditch the legacy permissions:
Android #
| Mechanism | Permission required |
|---|---|
MediaStore.ACTION_PICK_IMAGES (System Photo Picker) |
None |
Intent.ACTION_OPEN_DOCUMENT (Storage Access Framework) |
None |
Both run in a separate system process and grant per-item URI access to the
calling app. Do not add READ_MEDIA_IMAGES, READ_MEDIA_VIDEO,
READ_EXTERNAL_STORAGE, or WRITE_EXTERNAL_STORAGE to your manifest.
iOS #
| Mechanism | Permission required |
|---|---|
PHPickerViewController |
None (Apple docs) |
"Because the user must explicitly grant access to each photo or video they want to share through the picker, you don't need to request authorization to use this picker."
Do not add NSPhotoLibraryUsageDescription or
NSPhotoLibraryAddUsageDescription to Info.plist.
Architecture #
system_media_picker/
├── lib/
│ ├── system_media_picker.dart ← public exports
│ └── src/
│ ├── system_media_picker.dart ← MethodChannel facade
│ ├── exceptions.dart
│ ├── models/
│ │ ├── media_type.dart
│ │ └── picked_media.dart
│ └── controller/
│ └── media_picker_controller.dart
├── android/
│ └── src/main/kotlin/com/example/system_media_picker/
│ ├── SystemMediaPickerPlugin.kt ← FlutterPlugin + ActivityAware
│ └── SystemPhotoPickerHandler.kt ← Photo Picker / SAF logic
└── ios/
└── Classes/
└── SystemMediaPickerPlugin.swift ← PHPickerViewController bridge
Limitations #
- Cache-backed paths. The native side copies each chosen file into the
app's cache directory. Move them somewhere durable
(
getApplicationDocumentsDirectory()) if you need long-term persistence. - System cap (Android).
MediaStore.getPickImagesMaxLimit()may be lower than 20 on some devices — the more restrictive cap wins. - iOS 13. The picker throws
PickerUnsupportedOsException. There is noUIImagePickerControllerfallback on purpose: that path requiresNSPhotoLibraryUsageDescription. - Video previews. The plugin does not ship video-thumbnail
decoding. Drop in
video_thumbnailorvideo_playerif you need real frame thumbnails.
License #
MIT — see LICENSE.