Range Request

pub package License: BSD-3-Clause

A high-performance Dart package for HTTP range requests, enabling efficient file downloads with support for parallel chunked downloads, automatic resume capability, and checksum verification.

Features

  • ๐Ÿš€ Parallel chunked downloads - Split large files into chunks for concurrent downloading
  • ๐Ÿ”„ Automatic resume - Continue interrupted downloads from where they left off
  • โœ… Checksum verification - Verify file integrity with MD5/SHA256
  • ๐ŸŽฏ Smart fallback - Automatically falls back to serial download if server doesn't support range requests
  • ๐Ÿ“Š Progress tracking - Real-time download progress with customizable intervals
  • โŒ Cancellation support - Gracefully cancel downloads with CancelToken
  • ๐Ÿ”ง Highly configurable - Fine-tune chunk size, concurrency, retries, and more
  • ๐Ÿ—๏ธ Isolate-based I/O - Non-blocking file operations using Dart isolates
  • ๐Ÿงน Temporary file cleanup - Manual cleanup utility for incomplete downloads

Limitations and Advantages

Limitations

  • No background downloads on mobile: Since this package doesn't use native device features, downloads cannot continue when the app is in the background on mobile platforms. Downloads will pause when the app is backgrounded and can be resumed when the app returns to the foreground.

Advantages

  • Cross-platform consistency: Pure Dart implementation ensures identical behavior across iOS, Android, Web, and Desktop platforms with high-performance parallel downloads when servers support range requests
  • Minimal footprint: Depends only on http and crypto packages, keeping your app size small and reducing dependency vulnerabilities
  • Zero configuration: No native setup, platform-specific code, or complex configuration required - works out of the box on all platforms including Flutter Web
  • Maintenance-friendly: No platform-specific bugs or OS update compatibility issues to worry about - one codebase for all platforms

Installation

dart pub add range_request

Or add it manually to your pubspec.yaml:

dependencies:
  range_request:

Then run:

dart pub get

Usage

Basic Streaming Download

import 'package:range_request/range_request.dart';

void main() async {
  final client = RangeRequestClient();
  final url = Uri.parse('https://example.com/large-file.zip');

  // Stream download with automatic chunking if supported
  await for (final chunk in client.fetch(url)) {
    // Process each chunk (e.g., write to file, update UI, etc.)
    print('Received ${chunk.length} bytes');
  }
}

File Download with Progress Tracking

import 'package:range_request/range_request.dart';

void main() async {
  final downloader = FileDownloader.fromConfig(
    RangeRequestConfig(
      chunkSize: 5 * 1024 * 1024, // 5MB chunks
      maxConcurrentRequests: 4,
    ),
  );

  final result = await downloader.downloadToFile(
    Uri.parse('https://example.com/video.mp4'),
    '/downloads',
    outputFileName: 'my_video.mp4',
    onProgress: (bytes, total, status) {
      final progress = (bytes / total * 100).toStringAsFixed(1);
      print('Progress: $progress% - Status: $status');
    },
  );

  print('Downloaded to: ${result.filePath}');
  print('File size: ${result.fileSize} bytes');
}

Download with Resume Support

// Downloads will automatically resume if interrupted
final result = await downloader.downloadToFile(
  Uri.parse('https://example.com/large-file.iso'),
  '/downloads',
  resume: true, // Enable resume (default: true)
  onProgress: (bytes, total, status) {
    if (bytes > 0) {
      print('Resuming from ${bytes} bytes...');
    }
  },
);

Note: While resume support can continue interrupted downloads, it cannot detect file corruption that may occur during write operations (e.g., if the application crashes while writing buffered data to disk). In such cases, the resumed download may result in a corrupted file. Consider using checksum verification to ensure file integrity after completion.

Checksum Verification

final result = await downloader.downloadToFile(
  Uri.parse('https://example.com/software.exe'),
  '/downloads',
  checksumType: ChecksumType.sha256,
  onProgress: (bytes, total, status) {
    if (status == DownloadStatus.calculatingChecksum) {
      print('Verifying file integrity...');
    }
  },
);

print('SHA256: ${result.checksum}');

Using Cancellation Tokens

final cancelToken = CancelToken();

// Start download in a separate async operation
final downloadFuture = downloader.downloadToFile(
  Uri.parse('https://example.com/huge-file.bin'),
  '/downloads',
  cancelToken: cancelToken,
);

// Cancel the download after 5 seconds
Future.delayed(Duration(seconds: 5), () {
  cancelToken.cancel();
  print('Download cancelled');
});

try {
  await downloadFuture;
} on RangeRequestException catch (e) {
  if (e.code == RangeRequestErrorCode.cancelled) {
    print('Download was cancelled');
  }
}

Managing Multiple Downloads

You can manage multiple downloads through the FileDownloader's client instance:

final downloader = FileDownloader();

// Start multiple downloads
final download1 = downloader.downloadToFile(url1, '/downloads');
final download2 = downloader.downloadToFile(url2, '/downloads');
final download3 = downloader.downloadToFile(url3, '/downloads');

// Cancel all active downloads through the downloader's client
downloader.client.cancelAll();

// Or cancel and clear all tokens for a fresh start
downloader.client.cancelAndClear();

// Individual downloads can still use their own CancelToken
final token = CancelToken();
final download4 = downloader.downloadToFile(
  url4,
  '/downloads',
  cancelToken: token,
);
token.cancel(); // Cancel individual download

Configuration Options

final config = RangeRequestConfig(
  chunkSize: 10 * 1024 * 1024,        // 10MB chunks
  maxConcurrentRequests: 8,           // 8 parallel connections
  maxRetries: 3,                      // Retry failed chunks 3 times
  retryDelayMs: 1000,                 // Wait 1 second before retry
  connectionTimeout: Duration(seconds: 30),
  tempFileExtension: '.tmp',          // Extension for partial downloads
  headers: {                          // Custom headers
    'Authorization': 'Bearer token',
  },
);

final client = RangeRequestClient(config: config);

File Conflict Handling

// Choose how to handle existing files
final result = await downloader.downloadToFile(
  url,
  '/downloads',
  conflictStrategy: FileConflictStrategy.rename, // Creates "file(1).ext" if exists
  // or FileConflictStrategy.overwrite (default)
  // or FileConflictStrategy.error (throws exception)
);

Cleanup Temporary Files

// Remove incomplete downloads older than 1 day
final deletedCount = await downloader.cleanupTempFiles(
  '/downloads',
  olderThan: Duration(days: 1),
);
print('Cleaned up $deletedCount temporary files');

API Reference

Core Classes

  • RangeRequestClient: Main client for performing HTTP range requests

    • fetch(): Stream download with optional progress callback
    • checkServerInfo(): Check if server supports range requests
    • cancelAll(): Cancel all active operations
    • clearTokens(): Clear all tokens without cancelling
    • cancelAndClear(): Cancel all operations and clear tokens
  • FileDownloader: High-level file download operations

    • downloadToFile(): Download directly to file with resume support
    • cleanupTempFiles(): Clean up temporary download files
  • RangeRequestConfig: Configuration for download behavior

    • Chunk size, concurrency, retries, timeouts, and more
  • CancelToken: Token for cancelling download operations

    • cancel(): Cancel the associated download
    • isCancelled: Check if operation was cancelled

Enums

  • ChecksumType: Algorithm for file verification (sha256, md5, none)
  • DownloadStatus: Current operation status (downloading, calculatingChecksum)
  • FileConflictStrategy: How to handle existing files (overwrite, rename, error)

Advanced Features

Parallel Chunked Downloads

When the server supports range requests, files are automatically split into chunks and downloaded in parallel:

File: [====================] 100MB
       โ†“
Chunks: [====][====][====][====][====]  5 x 20MB
         โ†“  โ†“  โ†“  โ†“  โ†“
      Parallel Downloads (up to maxConcurrentRequests)
         โ†“  โ†“  โ†“  โ†“  โ†“
Reassembled: [====================]

Retry Logic

Failed chunks are automatically retried with exponential backoff:

  • First retry: 2 seconds delay (2x initial delay)
  • Second retry: 4 seconds delay (4x initial delay)
  • Third retry: 8 seconds delay (8x initial delay)

Memory Efficiency

  • Streaming API prevents loading entire files into memory
  • Configurable buffer sizes for optimal performance
  • Isolate-based checksum calculation prevents UI blocking

Error Handling

try {
  await downloader.downloadToFile(url, '/downloads');
} on RangeRequestException catch (e) {
  switch (e.code) {
    case RangeRequestErrorCode.networkError:
      print('Network connection failed');
    case RangeRequestErrorCode.serverError:
      print('Server returned an error');
    case RangeRequestErrorCode.fileError:
      print('File system error occurred');
    case RangeRequestErrorCode.checksumMismatch:
      print('Downloaded file is corrupted');
    case RangeRequestErrorCode.cancelled:
      print('Download was cancelled');
    case RangeRequestErrorCode.invalidResponse:
      print('Invalid response from server');
    case RangeRequestErrorCode.unsupportedOperation:
      print('Operation not supported');
  }
}

License

BSD 3-Clause License

Libraries

range_request