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 to true if successful
  • progress: Stream of ProgressInfo objects with real-time updates
  • taskId: 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 permission
  • unsupported-file-extension: File format not supported
  • network-error: Network connectivity issues
  • file-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.