SmartDownloader

smartdownloader is a Flutter package for reliable file downloads with support for direct URL downloads and automatic API response parsing.

This package now supports both:

  • Existing flows (DynamicDownloadManager, dashboard UI, retry callbacks)
  • New upgrade APIs (SmartDownloader.downloadFromResponse, SmartDownloader.sync)

Features

  • Backward-compatible existing APIs remain available
  • Direct download API (SmartDownloader.download)
  • Response-based download API (SmartDownloader.downloadFromResponse)
  • Auto-detect mode (SmartDownloader.sync)
  • Parse Map, List, nested maps, nested arrays, and mixed JSON
  • Wildcard collection paths like data.adSequences[*][*]
  • Validate URLs before starting downloads
  • Deduplicate URLs before download
  • Configurable concurrency (maxConcurrent, default 3)
  • Resume partial downloads using .part files
  • Restore unfinished tasks after app restart
  • Skip duplicate downloads when file already exists
  • Detect storage failures and fail gracefully
  • Retry failed downloads with network recovery behavior
  • Optional UI widgets:
    • DynamicDownloadDashboard (existing)
    • SmartDownloaderWidget (new, optional)

Installation

dependencies:
  smartdownloader: ^1.0.0

Import

import 'package:smartdownloader/smartdownloader.dart';

Backward Compatibility (Still Works)

All existing usage remains valid.

await SmartDownloader.download(
  url: 'https://example.com/file.mp4',
  fileName: 'file.mp4',
  customDownloadDir: '/my/path',
);

Existing DynamicDownloadManager APIs are still available:

final updatedJson = await DynamicDownloadManager.processResponse(
  jsonResponse: myJsonPayload,
  extractKeys: const ['store_image_url', 'adUrl'],
  customDownloadDir: '/my/path',
);

New API: downloadFromResponse

Use resolvers to map collection path + field names.

final items = await SmartDownloader.downloadFromResponse(
  response,
  customDownloadDir: '/my/path',
  collections: const [
    {
      'path': 'data.adSequences[*][*]',
      'url': 'adUrl',
      'title': 'name',
      'id': 'uuid',
      'type': 'mediaType',
      'size': 'contentLength',
      'folder': 'ads',
    },
    {
      'path': 'data.businessCategories[*]',
      'url': 'categoryImageUrl',
      'title': 'name',
      'id': 'uuid',
      'folder': 'categories',
    },
  ],
  maxConcurrent: 3,
  onItemUpdate: (item) {
    // item.status / item.progress updates
  },
  onTaskProgress: (url, progress, totalBytes) {},
);

New API: sync (Auto-Detect Mode)

final items = await SmartDownloader.sync(
  response,
  customDownloadDir: '/my/path',
  maxConcurrent: 3,
);

Auto-detected URL keys:

  • url
  • fileUrl
  • adUrl
  • imageUrl
  • categoryImageUrl
  • downloadUrl

Auto-detected metadata keys:

  • title/name: name, title
  • id: uuid, id

Optional UI (New)

SmartDownloaderWidget(
  items: items,
  onRetry: (item) {
    // Custom retry behavior
  },
)

Existing Dashboard UI (Still Works)

Important: pass all URL keys in extractKeys so the loop can identify every download URL and map JSON metadata correctly.
Example keys: adUrl, storeImage, categoryImageUrl, downloadUrl, imageUrl.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:smartdownloader/smartdownloader.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: DemoWrapper());
  }
}

class DemoWrapper extends StatefulWidget {
  const DemoWrapper({super.key});

  @override
  State<DemoWrapper> createState() => _DemoWrapperState();
}

class _DemoWrapperState extends State<DemoWrapper> {
  String? _downloadDir;

  final String myApiResponse = '''
  {
    "data": [
      {
        "id": 1,
        "store_image_url": "https://picsum.photos/id/237/200/300",
        "adUrl": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"
      }
    ]
  }
  ''';

  @override
  void initState() {
    super.initState();
    _initPath();
  }

  Future<void> _initPath() async {
    final dir = await getApplicationDocumentsDirectory();
    setState(() => _downloadDir = '${dir.path}/my_downloads');
  }

  @override
  Widget build(BuildContext context) {
    if (_downloadDir == null) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      body: DynamicDownloadDashboard(
        jsonResponse: jsonDecode(myApiResponse),
        // Pass all URL keys present in your API response
        extractKeys: const ['store_image_url', 'adUrl', 'storeImage', 'categoryImageUrl'],
        customDownloadDir: _downloadDir!,
        initialIsGridView: true,
        initialThemeMode: ThemeMode.dark,
      ),
    );
  }
}

Reliability Behavior

  • Internet lost mid-download: task moves to waiting state and continues when online
  • App closed/killed: task metadata is persisted and reused on next launch
  • Storage full: operation fails with graceful status (no corrupted final file)
  • Partial/corrupted downloads: writes to .part first, renames only after success
  • No duplicates: if target file already exists, download is skipped as completed

Notes

  • Resume depends on server support for HTTP Range; unsupported servers fall back to restart.
  • URL validation uses HEAD with fallback GET for servers that block HEAD.
  • Best results are for JSON-like API responses (Map/List/nested JSON). For non-JSON payloads, use direct download(...).
  • For deterministic extraction and richer mapping, always provide all URL field names in extractKeys.