Loading repository data…
Loading repository data…
importUsernameDev / repository
Flutter Universal Downloader is a powerful plugin designed to simplify file downloads in your Flutter applications. It leverages Android's foreground service to enable reliable background downloads with persistent notifications, providing users with real-time progress updates and the ability to cancel ongoing operations.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
A robust Flutter plugin designed for universal file downloading, offering reliable background operations with . This plugin empowers your applications to handle various file types (images, videos, documents, archives) seamlessly, providing users with and the ability to ongoing transfers. It abstracts away the complexities of native download managers and Android permission handling across different API levels, making file management in your app straightforward and efficient.
Minimum Requirements:
- Flutter: 3.10 or above
- Dart: 3.0 or above
- Android: minSdkVersion 21+
- iOS: Not officially supported for background/foreground downloads
DownloadProgress objects for UI updates.DownloadStatus enum describes download lifecycle.To integrate flutter_universal_downloader into your Flutter project, add it to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_universal_downloader: ^0.0.3 # Use the latest version from pub.dev
# Recommended for runtime permissions and device info (used in the example)
permission_handler: ^11.0.0 # Check pub.dev for the latest stable version
device_info_plus: ^10.0.0 # Check pub.dev for the latest stable version
After updating your pubspec.yaml, run:
flutter pub get
Open your android/app/src/main/AndroidManifest.xml and add these permissions inside the <manifest> tag:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
Even with AndroidManifest.xml entries, you must request some permissions at runtime. This is best done with permission_handler and device_info_plus:
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
Future<bool> checkAndRequestPermissions() async {
if (!Platform.isAndroid) {
return true;
}
int androidSdkVersion = 0;
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
androidSdkVersion = androidInfo.version.sdkInt;
} catch (e) {
print('Error fetching Android SDK version: $e');
return false;
}
if (androidSdkVersion >= 33) {
var status = await Permission.notification.request();
return status.isGranted;
} else if (androidSdkVersion >= 29) {
return true;
} else {
var status = await Permission.storage.request();
return status.isGranted;
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_universal_downloader/flutter_universal_downloader.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
class DownloadBtn extends StatefulWidget {
final String buttonText;
final String? fileName;
final String url;
const DownloadBtn({
Key? key,
required this.url,
required this.buttonText,
this.fileName,
}) : super(key: key);
@override
State<DownloadBtn> createState() => _DownloadBtnState();
}
class _DownloadBtnState extends State<DownloadBtn> {
bool _isOperationInProgress = false;
int _androidSdkVersion = 0;
@override
void initState() {
super.initState();
_getAndroidSdkVersion();
}
Future<void> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (mounted) {
setState(() => _androidSdkVersion = androidInfo.version.sdkInt);
}
} catch (e) {
debugPrint('Error fetching Android SDK version: $e');
}
}
}
Future<void> _handleDownload() async {
if (_isOperationInProgress) return;
if (mounted) setState(() => _isOperationInProgress = true);
debugPrint(
'[Download] Attempting download for: ${widget.fileName ?? "file"} from ${widget.url}',
);
try {
if (Platform.isAndroid) {
final PermissionStatus status;
if (_androidSdkVersion >= 33) {
status = await Permission.notification.request();
} else if (_androidSdkVersion >= 29) {
status = PermissionStatus.granted;
} else {
status = await Permission.storage.request();
}
if (!status.isGranted) {
_showSnackBar('❌ Permissions denied. Cannot download.');
return;
}
}
debugPrint(
'[Download] Permissions granted, initiating download.',
);
final success = await FlutterUniversalDownloader.foregroundDownload(
widget.url,
fileName: widget.fileName ?? 'downloaded_file',
);
_showSnackBar(
success
? '⬇️ Download for "${widget.fileName ?? "file"}" started!'
: '❌ Failed to start download for "${widget.fileName ?? "file"}".',
);
} on PlatformException catch (e) {
debugPrint(
'[Download] Platform Exception: ${e.message} (Code: ${e.code})',
);
_showSnackBar('⚠️ Download Error: ${e.message}');
} catch (e) {
debugPrint('[Download] Caught unexpected error: $e');
_showSnackBar('⚠️ An unexpected error occurred: ${e.toString()}');
} finally {
if (mounted) setState(() => _isOperationInProgress = false);
}
}
void _showSnackBar(String message) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ElevatedButton(
onPressed: _isOperationInProgress ? null : _handleDownload,
style: ElevatedButton.styleFrom(
minimumSize: const Size(50, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: _isOperationInProgress
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(widget.buttonText, style: const TextStyle(fontSize: 16)),
),
);
}
}
Usage in your main widget:
import 'package:flutter/material.dart';
import 'package:your_app_name/widgets/download_button.dart';
class MyHomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Universal Downloader Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DownloadBtn(
buttonText: 'Download PDF',
url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
fileName: 'sample_document.pdf',
),
DownloadBtn(
buttonText: 'Download Image',
url: 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png',
fileName: 'sample_image.png',
),
DownloadBtn(
buttonText: 'Download Video (Small)',
url: 'https://file-examples.com/storage/fe94537233649e7f53a1a45/2017/04/file_example_MP4_480_1_5MG.mp4',
fileName: 'sample_video.mp4',
),
],
),
),
);
}
}
You can listen to download progress and status with the progressStream:
import 'dart:async';
import 'dart:math' as math;
class MyDownloadScreen extends StatefulWidget {
@override
_MyDownloadScreenState createState() => _MyDownloadScreenState();
}
class _MyDownloadScreenState extends State<MyDownloadScreen> {
StreamSubscription<DownloadProgress>? _downloadProgressSubscription;
String _downloadStatusMessage = 'Ready to download';
double _currentDownloadProgress = 0.0; // 0.0 to 1.0
bool _isDownloadActive = false;
@override
void initState() {
super.initState();
_downloadProgressSubscription = FlutterUniversalDownloader.progressStream.listen(
(DownloadProgress progress) {
setState(() {
switch (progress.status) {
case DownloadStatus.progress:
_isDownloadActive = true;
_currentDownloadProgress = progress.totalBytes > 0
? progress.downloadedBytes / progress.totalBytes
: 0.0;
_downloadStatusMessage = 'Downloading "${progress.fileName}"... ${progress.progress}%