Nice Downloader
A powerful, easy-to-use file downloader for Flutter — downloads files fast using multiple parallel connections (like IDM), with pause/resume, speed limiting, and automatic recovery from network drops.
✨ What it can do
| ⚡ Fast downloads | Big files are split into up to 8 parallel connections automatically |
| ⏸️ Pause & Resume | Continue from the exact byte — even after closing the app |
| 🚦 Speed limit | Cap the download speed, change it live, or leave it at max |
| 🔁 Auto retry | Network dropped? It reconnects and continues by itself |
| 🛡️ Data safety | If the partial file gets damaged, it detects and re-downloads only the broken part |
| 🔌 Interceptors | Hook into every step (start, progress, complete, error…) for logging, analytics, etc. |
📦 Install
dependencies:
nice_downloader: ^1.3.0
🚀 Quick start (3 steps)
import 'package:nice_downloader/nice_downloader.dart';
// 1. Create ONE manager for your whole app
final manager = DownloadManager();
// 2. Create a download task
final task = await manager.createDownload(
url: 'https://example.com/video.mp4',
directory: '/storage/emulated/0/Download', // where to save
);
// 3. Listen to progress and start
task.progressStream.listen((progress) {
print('${progress.percent}% • ${progress.readableSpeed ?? ''}');
});
await task.start();
That's it! The file name is taken from the server or the URL automatically (you can also pass fileName: yourself).
📊 Showing progress
Every event on progressStream is a DownloadProgress with everything your UI needs:
task.progressStream.listen((progress) {
progress.status; // downloading, paused, completed...
progress.percent; // 0.0 .. 100.0
progress.downloadedBytes; // 5242880
progress.totalBytes; // 104857600
progress.readableDownloaded; // "5.0 MB" (nice for UI)
progress.readableTotal; // "100.0 MB"
progress.readableSpeed; // "2.3 MB/s" (null when not downloading)
progress.error; // the exception, when status == failed
});
You can also read the latest snapshot any time with task.progress (no stream needed).
Statuses
| Status | Meaning |
|---|---|
idle |
Created, not started yet |
connecting |
Opening the connection |
downloading |
Receiving bytes |
paused |
Stopped by you — resumable |
completed |
Finished successfully ✅ |
failed |
Error after all retries (see progress.error) |
canceled |
Canceled — file and saved state removed |
Handy helpers: status.isActive, status.isFinished, status.canStart.
⏯️ Pause, Resume, Cancel
await task.pause(); // stop, keep the partial file
await task.resume(); // continue from the same byte
await task.start(); // same as resume (also retries a failed download)
await task.cancel(); // stop + delete the partial file
Invalid calls are safe — pausing an idle task or starting a running one simply does nothing.
🚦 Speed limit
If you don't set anything, downloads run at max speed. Three ways to limit:
// 1. For all downloads:
DownloadManager(config: DownloadConfig(speedLimit: 2 * 1024 * 1024)); // 2 MB/s
// 2. For one download:
manager.createDownload(url: ..., directory: ..., speedLimit: 512 * 1024);
// 3. Change it WHILE downloading:
task.speedLimit = 1024 * 1024; // 1 MB/s
task.speedLimit = null; // back to max speed
The limit is the total speed — all parallel connections share it.
🔄 Restore downloads after app restart
Progress is saved automatically (using Hive). When your app starts:
final tasks = await manager.restorePersistedDownloads();
// completed ones show as completed, partial ones as paused
for (final task in tasks) {
// show in your UI; call task.start() to continue a paused one
}
A resumed download continues from where it stopped — even segmented ones continue every connection from its own offset.
🔐 Downloads that need login (headers)
manager.createDownload(
url: 'https://api.example.com/files/report.pdf',
directory: dir,
headers: {'Authorization': 'Bearer $token'},
);
⚙️ Configuration (all optional!)
Everything has a sensible default — only set what you want to change:
final manager = DownloadManager(
config: DownloadConfig(
// hook into download lifecycle (logging, analytics...)
interceptors: [LoggingInterceptor()],
// wait for internet instead of failing when offline
waitForConnection: true,
// how failed attempts retry (default: 3 retries, exponential backoff)
retryPolicy: ExponentialBackoffRetryPolicy(maxRetries: 5),
// parallel connections (default: up to 8, only for files >= 4 MB)
segmentPlanner: DefaultSegmentPlanner(maxSegments: 4),
// segmentPlanner: NoSegmentationPlanner(), // always 1 connection
// default speed cap in bytes/sec (default: null = max speed)
speedLimit: null,
// verify saved data before resuming (default: true)
verifyOnResume: true,
// how often progress events are emitted (default: 100 ms)
progressInterval: Duration(milliseconds: 250),
// don't save progress to disk (e.g. for tests)
// repository: InMemoryDownloadRepository(),
),
);
⚡ How the speed comes from (segmented downloads)
When the server supports it, big files are downloaded like IDM does:
File: 100 MB → 4 parallel connections
Connection 1 ──▶ bytes 0–25 MB ──┐
Connection 2 ──▶ bytes 25–50 MB ──┤── all write into ONE file,
Connection 3 ──▶ bytes 50–75 MB ──┤ each at its own position
Connection 4 ──▶ bytes 75–100 MB ──┘ (no merging needed)
You don't have to do anything — it's automatic, and it silently falls back to a normal single connection when:
- the server doesn't support range requests, or
- the file is small (< 4 MB by default)
If a server complains about too many connections (HTTP 429), the downloader automatically reduces them (8 → 4 → 2 → 1) and keeps going.
🔌 Interceptors — hook into every step
Want logging, notifications, analytics, or to modify requests? Extend DownloadInterceptor and override only what you need:
class MyInterceptor extends DownloadInterceptor {
@override
Future<DownloadRequest> onCreate(DownloadRequest request) async {
// runs BEFORE the download is built — you can modify the request!
return request.copyWith(
headers: {...request.headers, 'Authorization': 'Bearer ...'},
);
}
@override
Future<void> onComplete(DownloadTask task) async {
print('Saved to ${task.filePath}'); // show a notification, etc.
}
@override
Future<void> onError(DownloadTask task, Object error, StackTrace st) async {
// report to Crashlytics / Sentry
}
// also available: onStart, onResume, onPause, onCancel, onChunk
}
Register it once: DownloadConfig(interceptors: [MyInterceptor()]). A throwing interceptor never breaks a download.
❗ Error handling
All errors are typed and arrive in progress.error when status == failed:
if (progress.status == DownloadStatus.failed) {
final message = switch (progress.error) {
NoConnectionException() => 'No internet connection',
ServerException(statusCode: 403) => 'Access denied by the server',
ServerException(statusCode: 429) => 'Too many requests — try later',
ServerException(statusCode: final code) => 'Server error ($code)',
_ => 'Download failed',
};
}
Call task.start() to retry a failed download — it continues from the saved bytes.
🧰 Advanced: swap any part
Every piece is an interface you can replace via DownloadConfig — no core code changes needed:
| Interface | Default | Replace it to… |
|---|---|---|
DownloadClient |
package:http |
use Dio, a proxy, mock in tests |
DownloadRepository |
Hive CE | store state in SQLite, memory, … |
ConnectivityChecker |
DNS lookup | use connectivity_plus |
RetryPolicy |
exponential backoff | custom retry rules |
SegmentPlanner |
≤ 8 parts of ≥ 2 MB | custom splitting logic |
DownloadFileWriter |
RandomAccessFile |
encrypted storage, … |
🛟 Troubleshooting
The download fails with 403 — some sites (e.g. behind Cloudflare's "Just a moment…" page) block all download managers and require a real browser. Not fixable from any downloader. For sites you're logged into, pass your browser's cookies via headers: {'Cookie': '...'}.
The download fails with 429 — the server limits connections/requests per IP. The downloader adapts automatically; if it still fails, lower maxSegments or wait a bit.
Good links for testing:
https://proof.ovh.net/files/100Mb.dat
https://ash-speed.hetzner.com/100MB.bin
http://ipv4.download.thinkbroadband.com/50MB.zip
📱 Full example
The example app is a complete mini download manager (URL input, progress cards, pause/continue, speed controls, restore on startup) — a great starting point for your own UI.
📄 License
Libraries
- nice_downloader
- A clean, extensible file downloader for Flutter.