media_gallery_saver
A Flutter plugin that allows you to save files to the iOS and Android galleries from URL, asset, and file. The plugin supports Stream-based progress tracking for network downloads with concurrent download support and handles permissions automatically.
Supported File Formats
| Type | Formats |
|---|---|
| Images | jpg, jpeg, png, gif, webp, heic |
| Videos | mp4, mov |
Installation
Add this to your pubspec.yaml:
dependencies:
media_gallery_saver: ^1.3.1
Then run:
flutter pub get
Platform Setup
iOS
Add the following permission to your ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to save images and videos</string>
Android
The required permissions are automatically included in the plugin's AndroidManifest.xml:
WRITE_EXTERNAL_STORAGE(for Android < 10)READ_EXTERNAL_STORAGE
Usage
Basic Usage
import 'package:media_gallery_saver/media_gallery_saver.dart';
// Create an instance
final saver = MediaGallerySaver();
// Save from URL with progress tracking
final result = saver.saveMediaFromUrl(
url: 'https://example.com/image.jpg',
);
// Listen to progress updates
result.progress.listen((progress) {
print('Progress: ${(progress.progress * 100).toInt()}%');
});
// Wait for completion
final success = await result.completion;
// Save from local file (no progress tracking needed)
File file = File('path/to/your/file.jpg');
bool success = await saver.saveMediaFromFile(file: file);
// Save from asset (no progress tracking needed)
bool success = await saver.saveMediaFromAsset(
rootBundle: rootBundle,
assetName: 'assets/images/sample.png',
);
Stream-based Progress Tracking
Track download progress for network files with real-time streams:
final result = saver.saveMediaFromUrl(
url: 'https://example.com/large-video.mp4',
);
// Listen to progress updates
result.progress.listen((progress) {
print('Task ${result.taskId}: ${(progress.progress * 100).toInt()}%');
print('Downloaded: ${progress.downloadedBytes}/${progress.totalBytes} bytes');
// Update your UI here
setState(() {
_downloadProgress = progress.progress;
});
});
// Wait for completion
final success = await result.completion;
Complete Example with Progress UI
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:media_gallery_saver/media_gallery_saver.dart';
class DownloadScreen extends StatefulWidget {
@override
_DownloadScreenState createState() => _DownloadScreenState();
}
class _DownloadScreenState extends State<DownloadScreen> {
double _progress = 0.0;
bool _isDownloading = false;
String _statusMessage = '';
StreamSubscription? _progressSubscription;
Future<void> _downloadAndSave() async {
setState(() {
_progress = 0.0;
_isDownloading = true;
_statusMessage = 'Starting download...';
});
try {
final result = MediaGallerySaver().saveMediaFromUrl(
url: 'https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4',
);
// Listen to progress updates
_progressSubscription = result.progress.listen((progress) {
setState(() {
_progress = progress.progress;
_statusMessage = 'Downloading... ${(progress.progress * 100).toInt()}%';
});
});
// Wait for completion
final success = await result.completion;
setState(() {
_isDownloading = false;
_statusMessage = success ? 'Download completed!' : 'Download failed';
});
} catch (e) {
setState(() {
_isDownloading = false;
_statusMessage = 'Error: $e';
});
} finally {
_progressSubscription?.cancel();
_progressSubscription = null;
}
}
@override
void dispose() {
_progressSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Media Gallery Saver')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isDownloading) ...[
LinearProgressIndicator(value: _progress),
SizedBox(height: 16),
],
Text(_statusMessage, style: TextStyle(fontSize: 16)),
SizedBox(height: 32),
ElevatedButton(
onPressed: _isDownloading ? null : _downloadAndSave,
child: Text('Download and Save'),
),
],
),
),
);
}
}
Configuration Options
Customize network behavior with MediaGallerySaverOptions:
final saver = MediaGallerySaver(
options: MediaGallerySaverOptions(
connectTimeout: 30000, // Connection timeout: 30 seconds
readTimeout: 120000, // Read timeout: 2 minutes
),
);
Available Options
| Option | Type | Default | Description |
|---|---|---|---|
connectTimeout |
int |
15000 |
Connection timeout in milliseconds |
readTimeout |
int |
0 |
Read timeout in milliseconds (0 = unlimited) |
API Reference
MediaGallerySaver Methods
saveMediaFromUrl
Save media from a network URL with Stream-based progress tracking.
MediaSaveResult saveMediaFromUrl({
required String url,
int quality = 100,
})
Returns a MediaSaveResult containing:
completion: Future that resolves totrueif successfulprogress: Stream ofProgressInfoobjects with real-time updatestaskId: Unique identifier for this operation
saveMediaFromFile
Save media from a local file.
Future<bool> saveMediaFromFile({
required File file,
int quality = 100,
})
saveMediaFromAsset
Save media from app assets.
Future<bool> saveMediaFromAsset({
required AssetBundle rootBundle,
required String assetName,
int quality = 100,
})
Parameters
quality: Image quality (1-100, default: 100). Only affects image files.
Concurrent Downloads
With the Stream-based API, concurrent downloads work seamlessly with a single instance:
final saver = MediaGallerySaver();
// ✅ Multiple concurrent downloads with independent progress tracking
final result1 = saver.saveMediaFromUrl(url: url1);
final result2 = saver.saveMediaFromUrl(url: url2);
// Each has its own progress stream
result1.progress.listen((progress) => print('Download 1: ${progress.progress}'));
result2.progress.listen((progress) => print('Download 2: ${progress.progress}'));
// Wait for both to complete
final results = await Future.wait([
result1.completion,
result2.completion,
]);
Advanced Stream Operations
Take advantage of Dart's powerful Stream API:
final result = saver.saveMediaFromUrl(url: url);
// Filter progress updates (only show every 10%)
result.progress
.where((progress) => (progress.progress * 100).toInt() % 10 == 0)
.listen((progress) => print('${(progress.progress * 100).toInt()}%'));
// Transform progress data
result.progress
.map((progress) => 'Downloaded ${progress.downloadedBytes} of ${progress.totalBytes} bytes')
.listen(print);
// Multiple listeners for different purposes
result.progress.listen((progress) => updateUI(progress)); // UI updates
result.progress.listen((progress) => logProgress(progress)); // Logging
result.progress.listen((progress) => sendAnalytics(progress)); // Analytics
Error Handling
The plugin throws MediaGallerySaverException for various error conditions:
try {
final result = saver.saveMediaFromUrl(url: 'https://example.com/image.jpg');
// Handle progress
result.progress.listen(
(progress) => print('Progress: ${progress.progress}'),
onError: (error) => print('Progress stream error: $error'),
);
// Wait for completion
final success = await result.completion;
print('Download successful: $success');
} on MediaGallerySaverException catch (e) {
print('Error: ${e.message} (Code: ${e.code})');
} catch (e) {
print('Unexpected error: $e');
}
Common Error Codes
unauthorized: User denied gallery access permissionunsupported-file-extension: File format not supportednetwork-error: Network connectivity issuesfile-not-found: Local file doesn't exist
Requirements
- Flutter: ≥ 3.3.0
- Dart: ≥ 3.3.0
- iOS: ≥ 11.0
- Android: API level 21+ (Android 5.0+)
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.