meta_photo_picker 0.0.1
meta_photo_picker: ^0.0.1 copied to clipboard
A privacy-first Flutter plugin for picking photos with detailed metadata. Uses PHPicker on iOS (no permission required) and wechat_assets_picker on Android.
meta_photo_picker #
A modern, privacy-focused Flutter plugin for picking photos from the device gallery with detailed metadata.
đ Introduction #
meta_photo_picker provides a unified API for photo selection across iOS and Android platforms:
- iOS: Uses Apple's privacy-preserving PHPicker (iOS 14+) - no permission required!
- Android: Uses the popular wechat_assets_picker for a native experience
The plugin returns detailed metadata for each selected photo including file information, dimensions, and image data, making it perfect for apps that need more than just image selection.
⨠Features #
Core Features #
- đŧī¸ Pick single or multiple photos from the device gallery
- đ Privacy-first on iOS - PHPicker doesn't require photo library permission
- đ Rich metadata for each selected photo
- đ¨ Native UI on both platforms
- ⥠Fast and efficient - optimized for performance
- đ§ Highly configurable picker options
Metadata Included #
- File name, size (bytes and formatted), and type (JPEG, PNG, HEIC, etc.)
- Image dimensions (width, height) and aspect ratio
- Creation date (ISO 8601 format)
- Asset identifier (for photo library reference)
- Image data (raw bytes for display)
- Image orientation and scale factor
Configuration Options #
- Selection limit (single, multiple, or unlimited)
- Media type filter (images, videos, live photos, or all)
- Asset representation mode (automatic, current, compatible)
- Compression quality (0.0 - 1.0, default 1.0 = no compression)
Additional Features #
- đą Check photo access status - Know if user has granted full, limited, or no access
- đ Direct selection on Android - Tap to select, no preview needed
- đ¯ Type-safe models - Well-defined Dart models for all data
- đ Comprehensive documentation - Clear examples and API docs
đ Platform Differences #
| Feature | iOS (PHPicker) | Android (wechat_assets_picker) |
|---|---|---|
| Permission Required | â No | â Yes |
| Privacy | â Privacy-preserving | â ī¸ Requires storage access |
| Limited Access | â Supported (iOS 14+) | â Not available |
| UI | Native iOS picker | Material Design picker |
| Selection Speed | Fast | Fast (optimized) |
| Context Required | â No | â Yes |
đą Platform Support #
| Platform | Minimum Version | Implementation |
|---|---|---|
| iOS | iOS 14.0+ | PHPicker (privacy-preserving) |
| Android | API 21+ (Android 5.0+) | wechat_assets_picker |
đĻ Installation #
Add this to your package's pubspec.yaml file:
dependencies:
meta_photo_picker: ^0.0.1
Then run:
flutter pub get
âī¸ Platform Setup #
iOS Setup #
1. Add permission to ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to select and display photos.</string>
Note: Even though PHPicker doesn't require permission for basic photo selection, this description is still needed in Info.plist for App Store submission.
2. Minimum iOS version:
Ensure your ios/Podfile has iOS 14.0 or higher:
platform :ios, '14.0'
Android Setup #
The plugin uses wechat_assets_picker for Android, which requires proper permission configuration.
1. Add permissions to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- For Android 12 and below (API 32-) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- For Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Optional: For accessing photo location metadata -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<application ...>
...
</application>
</manifest>
2. Update android/app/build.gradle:
android {
compileSdkVersion 34 // or higher
defaultConfig {
applicationId "com.example.yourapp"
minSdkVersion 21 // Minimum API 21
targetSdkVersion 34 // Target latest
}
}
3. Update android/gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
4. Update android/build.gradle:
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0' // or higher
}
Permission Handling:
- The plugin automatically requests the appropriate permission based on Android version
- Android 13+ (API 33+): Uses
READ_MEDIA_IMAGES - Android 12 and below: Uses
READ_EXTERNAL_STORAGE - Permission is requested when
pickPhotos()is called
đ Usage #
Quick Start #
import 'package:meta_photo_picker/meta_photo_picker.dart';
final picker = MetaPhotoPicker();
// Pick a single photo
final photo = await picker.pickSinglePhoto(context: context);
// Pick multiple photos
final photos = await picker.pickPhotos(context: context);
Basic Example - Pick Single Photo #
import 'package:flutter/material.dart';
import 'package:meta_photo_picker/meta_photo_picker.dart';
class MyWidget extends StatelessWidget {
final picker = MetaPhotoPicker();
Future<void> pickPhoto(BuildContext context) async {
// Pick a single photo (context required for Android)
final photo = await picker.pickSinglePhoto(context: context);
if (photo != null) {
print('â
Selected: ${photo.fileName}');
print('đĻ Size: ${photo.fileSize}');
print('đ Dimensions: ${photo.dimensions.width}x${photo.dimensions.height}');
print('đ¨ Type: ${photo.fileType}');
// Display the image
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Image.memory(photo.imageData),
),
);
}
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => pickPhoto(context),
child: Text('Pick Photo'),
);
}
}
Pick Multiple Photos with Configuration #
import 'package:meta_photo_picker/meta_photo_picker.dart';
final picker = MetaPhotoPicker();
// Configure picker options
final config = PickerConfig(
selectionLimit: 5, // Pick up to 5 photos (0 = unlimited)
filter: PickerFilter.images, // Only show images
preferredAssetRepresentationMode: AssetRepresentationMode.current,
compressionQuality: 1.0, // No compression (original quality)
);
// Pick multiple photos
final photos = await picker.pickPhotos(
config: config,
context: context, // Required for Android
);
if (photos != null && photos.isNotEmpty) {
print('đ¸ Picked ${photos.length} photos');
for (var photo in photos) {
print('---');
print('đ File: ${photo.fileName}');
print('đĻ Size: ${photo.fileSize}');
print('đ¨ Type: ${photo.fileType}');
print('đ Dimensions: ${photo.dimensions.width}x${photo.dimensions.height}');
print('đ
Created: ${photo.creationDate}');
print('đ ID: ${photo.id}');
}
}
Display Selected Photos #
import 'package:flutter/material.dart';
import 'package:meta_photo_picker/meta_photo_picker.dart';
class PhotoGallery extends StatefulWidget {
@override
State<PhotoGallery> createState() => _PhotoGalleryState();
}
class _PhotoGalleryState extends State<PhotoGallery> {
final picker = MetaPhotoPicker();
List<PhotoInfo> selectedPhotos = [];
Future<void> pickPhotos() async {
final photos = await picker.pickPhotos(
context: context,
config: PickerConfig(selectionLimit: 0), // Unlimited
);
if (photos != null) {
setState(() {
selectedPhotos.addAll(photos);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Photo Gallery')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: selectedPhotos.length,
itemBuilder: (context, index) {
final photo = selectedPhotos[index];
return Image.memory(
photo.imageData,
fit: BoxFit.cover,
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: pickPhotos,
child: Icon(Icons.add_photo_alternate),
),
);
}
}
Check Photo Access Status #
Check if the user has granted photo library access (useful for showing UI hints):
final picker = MetaPhotoPicker();
// Check access status
final status = await picker.checkPhotoAccessStatus();
switch (status) {
case PhotoAccessStatus.fullAccess:
print('â
Full access granted');
break;
case PhotoAccessStatus.limitedAccess:
print('â ī¸ Limited access (iOS only - user selected specific photos)');
break;
case PhotoAccessStatus.noAccess:
print('â No access - need to request permission');
break;
}
// Use in your UI
if (status == PhotoAccessStatus.noAccess) {
// Show a message to the user
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Permission Needed'),
content: Text('Please grant photo access to select images.'),
),
);
}
Note:
- On iOS with PHPicker, this will always return
fullAccessorlimitedAccesssince no permission is required - On Android, this checks the actual storage permission status
limitedAccessis only available on iOS 14+ when user selects specific photos
đ API Reference #
Configuration Options #
PickerConfig
PickerConfig({
int selectionLimit = 1, // Number of photos to select (0 = unlimited)
PickerFilter filter = PickerFilter.images, // Media type filter
AssetRepresentationMode preferredAssetRepresentationMode = AssetRepresentationMode.current,
double compressionQuality = 1.0, // JPEG compression (0.0 - 1.0)
})
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
selectionLimit |
int |
1 |
Maximum number of photos to select. Set to 0 for unlimited. |
filter |
PickerFilter |
images |
Filter media types shown in picker. |
preferredAssetRepresentationMode |
AssetRepresentationMode |
current |
How assets should be represented (iOS only). |
compressionQuality |
double |
1.0 |
JPEG compression quality (1.0 = no compression). |
PickerFilter Options
| Value | Description | Platform Support |
|---|---|---|
PickerFilter.images |
Show only images (JPEG, PNG, HEIC, etc.) | iOS & Android |
PickerFilter.videos |
Show only videos | iOS & Android |
PickerFilter.livePhotos |
Show only live photos | iOS only (falls back to images on Android) |
PickerFilter.any |
Show all media types | iOS & Android |
AssetRepresentationMode Options
| Value | Description | Use Case |
|---|---|---|
automatic |
System decides best format | Let iOS choose optimal format |
current |
Use current format (e.g., HEIC) | Preserve original format |
compatible |
Convert to compatible format (JPEG) | Ensure compatibility |
Note: AssetRepresentationMode only affects iOS. Android always returns the original format.
PhotoInfo Model #
Each selected photo returns a PhotoInfo object with comprehensive metadata:
class PhotoInfo {
final String id; // Unique identifier
final String fileName; // File name (e.g., "IMG_1234.jpg")
final int fileSizeBytes; // Size in bytes (e.g., 2547891)
final String fileSize; // Formatted size (e.g., "2.43 MB")
final PhotoDimensions dimensions; // Width and height
final String? creationDate; // ISO 8601 format (e.g., "2024-01-15T10:30:00Z")
final String fileType; // File type: "JPEG", "PNG", "HEIC", "GIF", "WEBP"
final String? assetIdentifier; // Photos library asset ID (platform-specific)
final Uint8List imageData; // Raw image bytes for display
final double scale; // Image scale factor (typically 1.0)
final ImageOrientation orientation; // Image orientation (up, down, left, right, etc.)
// Computed properties
double get aspectRatio; // Width / height ratio
}
PhotoDimensions:
class PhotoDimensions {
final int width; // Image width in pixels
final int height; // Image height in pixels
}
ImageOrientation:
enum ImageOrientation {
up, // Normal orientation
down, // Rotated 180°
left, // Rotated 90° counter-clockwise
right, // Rotated 90° clockwise
upMirrored, // Flipped horizontally
downMirrored, // Flipped horizontally and rotated 180°
leftMirrored, // Flipped horizontally and rotated 90° counter-clockwise
rightMirrored,// Flipped horizontally and rotated 90° clockwise
}
Example Usage:
final photo = await picker.pickSinglePhoto(context: context);
if (photo != null) {
// File information
print('File: ${photo.fileName}'); // "IMG_1234.HEIC"
print('Size: ${photo.fileSize}'); // "2.43 MB"
print('Type: ${photo.fileType}'); // "HEIC"
// Image properties
print('Width: ${photo.dimensions.width}'); // 4032
print('Height: ${photo.dimensions.height}'); // 3024
print('Aspect: ${photo.aspectRatio}'); // 1.333
print('Orientation: ${photo.orientation}'); // ImageOrientation.up
// Display the image
Image.memory(photo.imageData);
// Save to file
final file = File('path/to/save/${photo.fileName}');
await file.writeAsBytes(photo.imageData);
}
đ¯ Complete Example App #
See the example directory for a complete working app that demonstrates:
- â Picking single and multiple photos
- â Displaying selected photos in a grid
- â Showing detailed photo information (size, dimensions, type, etc.)
- â Deleting individual photos
- â Clearing all photos
- â Permission status checking
- â Modern Material Design UI
- â Error handling
Run the example:
cd example
flutter run
đĄī¸ Error Handling #
Basic Error Handling #
try {
final photos = await picker.pickPhotos(context: context);
if (photos != null && photos.isNotEmpty) {
print('â
Selected ${photos.length} photos');
} else {
print('âšī¸ User cancelled or no photos selected');
}
} on PlatformException catch (e) {
print('â Platform error: ${e.code} - ${e.message}');
} catch (e) {
print('â Unexpected error: $e');
}
Comprehensive Error Handling #
Future<void> pickPhotosWithErrorHandling(BuildContext context) async {
try {
final picker = MetaPhotoPicker();
// Check access status first (optional)
final status = await picker.checkPhotoAccessStatus();
if (status == PhotoAccessStatus.noAccess) {
// Show permission explanation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Permission Needed'),
content: Text('Please grant photo access to select images.'),
),
);
return;
}
// Pick photos
final photos = await picker.pickPhotos(
context: context,
config: PickerConfig(selectionLimit: 10),
);
if (photos != null && photos.isNotEmpty) {
// Success!
print('â
Selected ${photos.length} photos');
// Process photos...
} else {
// User cancelled
print('âšī¸ User cancelled selection');
}
} on PlatformException catch (e) {
// Platform-specific errors
String message;
switch (e.code) {
case 'PERMISSION_DENIED':
message = 'Permission denied. Please grant photo access.';
break;
case 'UNSUPPORTED_VERSION':
message = 'This feature requires iOS 14 or later.';
break;
default:
message = 'Error: ${e.message ?? "Unknown error"}';
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
} catch (e) {
// Unexpected errors
print('â Unexpected error: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred')),
);
}
}
}
â ī¸ Important Notes #
iOS Specific #
- â No permission required - PHPicker is privacy-preserving
- â Works immediately - No permission dialog for basic photo selection
- â ī¸ iOS 14+ required - PHPicker is not available on older iOS versions
- âšī¸ Limited access - iOS 14+ users can grant access to selected photos only
- âšī¸ Creation date - Uses current date as fallback (no PHAsset access needed)
Android Specific #
- â ī¸ Permission required - Must grant storage/media permission
- â ī¸ Context required - BuildContext must be passed to
pickPhotos() - â Auto-permission - Plugin automatically requests appropriate permission
- â
Version-aware - Uses
READ_MEDIA_IMAGESon Android 13+,READ_EXTERNAL_STORAGEon older versions - â Direct selection - Tap to select, no preview needed
Memory Considerations #
- â ī¸ Image data in memory - Each
PhotoInfocontains full image bytes - đĄ Large images - Consider memory usage when selecting many large photos
- đĄ Optimization - Process and save images to disk, then clear from memory
- đĄ Compression - Use
compressionQualityparameter to reduce memory usage
Platform Differences #
| Feature | iOS | Android |
|---|---|---|
| Permission dialog | â Not shown | â Shown on first use |
| Limited access | â Supported | â Not available |
| Context required | â No | â Yes |
| Live Photos | â Supported | â Falls back to images |
| Creation date | â ī¸ Fallback to current | â Actual date |
đ Best Practices #
1. Always Check Context Availability #
// â
Good
if (context.mounted) {
final photos = await picker.pickPhotos(context: context);
}
// â Bad
final photos = await picker.pickPhotos(context: context);
// Context might be unmounted
2. Handle Null Results #
// â
Good
final photos = await picker.pickPhotos(context: context);
if (photos != null && photos.isNotEmpty) {
// Process photos
}
// â Bad
final photos = await picker.pickPhotos(context: context);
for (var photo in photos) { // Might throw if null
// ...
}
3. Manage Memory for Large Selections #
// â
Good - Process and save immediately
final photos = await picker.pickPhotos(context: context);
if (photos != null) {
for (var photo in photos) {
// Save to disk
final file = File('path/${photo.fileName}');
await file.writeAsBytes(photo.imageData);
}
// Clear from memory
photos.clear();
}
// â Bad - Keep all in memory
final allPhotos = <PhotoInfo>[];
final photos = await picker.pickPhotos(context: context);
if (photos != null) {
allPhotos.addAll(photos); // Memory grows indefinitely
}
4. Provide User Feedback #
// â
Good - Show loading and feedback
setState(() => isLoading = true);
final photos = await picker.pickPhotos(context: context);
setState(() => isLoading = false);
if (photos != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added ${photos.length} photo(s)')),
);
}
â FAQ #
Q: Do I need to request permission on iOS? A: No! PHPicker is privacy-preserving and doesn't require photo library permission. The picker is handled by the system.
Q: Why do I need to pass BuildContext? A: BuildContext is required for Android to show the picker dialog. iOS doesn't need it but we keep the API consistent.
Q: Can I get the actual creation date on iOS? A: Not without requesting photo library permission. To keep the plugin permission-free on iOS, we use the current date as a fallback.
Q: What's the difference between fullAccess and limitedAccess?
A: On iOS 14+, users can choose to give apps access to all photos (fullAccess) or only selected photos (limitedAccess). Both work with PHPicker.
Q: How do I handle large images?
A: Consider using the compressionQuality parameter to reduce file size, or process and save images to disk immediately after selection.
Q: Does this work with videos?
A: Yes! Use PickerFilter.videos or PickerFilter.any to include videos. The plugin returns video data the same way as images.
Q: Can I customize the picker UI? A: No. Both PHPicker (iOS) and wechat_assets_picker (Android) use system/native UI for consistency and security.
đ§ Troubleshooting #
iOS Issues #
"PHPicker requires iOS 14 or later"
- Ensure your
ios/Podfilehasplatform :ios, '14.0'or higher - Run
cd ios && pod install
Images not loading
- Check that
NSPhotoLibraryUsageDescriptionis inInfo.plist - Verify the app has been rebuilt after adding the permission
Android Issues #
"Permission denied"
- Ensure permissions are in
AndroidManifest.xml - Check that
compileSdkVersionis 33 or higher - Verify Gradle version is 8.0 or higher
"Context is required for Android picker"
- Always pass
contextparameter:picker.pickPhotos(context: context)
Gradle build errors
- Update Gradle to 8.0+
- Update Android Gradle Plugin to 8.1.0+
- Ensure
minSdkVersionis 21 or higher
đ¤ Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup #
# Clone the repository
git clone https://github.com/yourusername/meta_photo_picker.git
# Install dependencies
cd meta_photo_picker
flutter pub get
# Run example app
cd example
flutter run
# Run tests
cd ..
flutter test
đ License #
This project is licensed under the MIT License - see the LICENSE file for details.
đ Acknowledgments #
- Built with Apple's PHPicker framework for iOS
- Uses wechat_assets_picker for Android
- Uses permission_handler for permission management
đ Additional Resources #
Made with â¤ī¸ for the Flutter community
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments #
Based on Apple's PHPicker framework for iOS.