Flutter File Downloads with Dio

Flutter File Downloads with Dio 5.9.0 in 2025 – Progress, Notifications & Error Handling

Why Efficient File Downloads Matter in Flutter Apps

As a Flutter developer, I’ve seen apps slow down or crash when downloads aren’t handled properly. Efficient downloads ensure your users don’t wait forever and stay informed about progress. This is especially important for media apps, e-learning platforms, or file-sharing tools.

Overview of Dio 5.9.0 for HTTP Networking

Dio is my go-to HTTP client in Flutter. Version 5.9.0 comes with high-performance downloading, request cancellation, interceptors, and easy error handling. It makes building a reliable downloader much smoother than using the default http package.

Before starting any download, it’s a good idea to check the device’s internet connection. That’s where the Flutter Internet Connectivity Checker comes in — it ensures your app only tries to download files when the network is available, preventing errors and improving user experience.

What This Tutorial Will Cover

In this tutorial, I’ll show you how to:

  • Download files efficiently using Dio
  • Show real-time progress notifications in the app and system tray
  • Handle errors and interruptions gracefully
  • Save files in the right directories for Android and iOS
  • Open files directly from notifications

Table of Contents

  1. Introduction
  2. Why Efficient File Downloads Matter in Flutter Apps
  3. Understanding Dio 5.9.0 in Flutter
    • What is Dio?
    • Key Features and Benefits
    • Why Use Dio Over Other HTTP Clients
    • Dio 5.9.0 New Features & Improvements
  4. Setting Up Your Flutter Project for File Downloads
    • Required Packages
    • Installing Dependencies
    • Required Permissions
  5. Building the File Downloader Logic
    • Creating the Download Function
    • Saving Files to Correct Directories
    • Handling Download Errors
  6. Adding Progress Notifications
    • Setting Up flutter_local_notifications
    • Showing Real-Time Download Progress
    • Showing Download Completion Notification
  7. Bonus: Showing Download Progress in the App UI
    • Using ValueNotifier & ValueListenableBuilder
    • Advanced Tips & Best Practices
  8. Conclusion
    • Next Steps: Optimization and Enhancements
    • FAQs

Understanding Dio 5.9.0 in Flutter

What is Dio?

Dio is a powerful HTTP client for Dart and Flutter that makes networking much easier. Think of it as an advanced version of Flutter’s default http package. With Dio, you can:

  • Perform GET, POST, PUT, DELETE requests easily
  • Handle file uploads and downloads efficiently
  • Add request interceptors for logging, caching, or modifying requests
  • Cancel requests if needed
  • Handle timeouts, retries, and errors gracefully

Basically, it gives you full control over network communication in your Flutter apps.

Key Features and Benefits

As a Flutter developer, I love using Dio because it simplifies many networking tasks that are otherwise complicated:

  1. Interceptors: Log requests/responses, add authentication tokens, or handle errors in one place.
  2. Timeouts: Set custom connection and response timeouts easily.
  3. Cancellation: Stop long-running requests with CancelToken.
  4. File handling: Download and upload files with progress tracking.
  5. Flexible request options: Customize headers, response types, and query parameters.

Why Use Dio Over Other HTTP Clients?

While Flutter’s http package is simple, it lacks many features that are critical for production apps. Here’s why Dio is better:

  • Progress tracking: Track uploads and downloads in real-time.
  • Error handling: Provides detailed DioException with status codes, messages, and stack traces.
  • Interceptors: Easily modify requests or responses globally without changing each call.
  • File download support: Supports ResponseType.bytes or streaming downloads.
  • Highly customizable: Headers, timeout, base URLs, query parameters—all configurable globally or per request.

Dio 5.9.0 New Features & Improvements

Dio 5.9.0 brings several improvements that are especially useful for file downloads and networking in Flutter:

  1. Updated handling for ResponseType.bytes and streamed downloads
  2. Better DioException messages for debugging network errors
  3. Improved interceptors system for sequential and queued requests
  4. Enhanced support for canceling requests and tracking progress
  5. Compatibility with modern Flutter and Dart versions

Highlights from the Latest Version

Some notable updates that make file downloads easier in Flutter:

  • Streamlined file download: Using dio.download() now supports saving large files efficiently.
  • Enhanced error handling: Catch detailed network exceptions easily with try-catch blocks.
  • Better async request management: You can now queue requests or handle multiple downloads in parallel without blocking the UI.

Relevant Changes for File Downloads

If your main goal is downloading files in Flutter, here’s what you need to know:

  • Use ResponseType.bytes to download binary data like PDFs, images, or videos.
  • Track progress using onReceiveProgress, which provides the number of bytes downloaded and total bytes.
  • Use DioException to handle failed downloads and provide meaningful feedback to users.

Example: Basic File Download with Dio 5.9.0

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';

final dio = Dio();

Future downloadFile(String url, String fileName) async {
  try {
    // Get app directory to save file
    final dir = await getApplicationDocumentsDirectory();
    final savePath = '${dir.path}/$fileName';

    // Start downloading
    await dio.get(
      url,
      options: Options(responseType: ResponseType.bytes),
      onReceiveProgress: (received, total) {
        if (total != -1) {
          print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
        }
      },
    ).then((response) async {
      final file = File(savePath);
      await file.writeAsBytes(response.data);
      print('File saved to $savePath');
    });
  } catch (e) {
    print('Download failed: $e');
  }
}

In this example:

  • We use getApplicationDocumentsDirectory() to save the file safely on both Android and iOS.
  • ResponseType.bytes ensures the file is downloaded as binary data.
  • onReceiveProgress prints download progress in the console.

Setting Up Your Flutter Project for File Downloads

Required Packages

These packages work together to make downloading files in your Flutter app smooth and user-friendly.

  • Dio takes care of fetching files from the internet quickly and reliably.

  • flutter_local_notifications lets you show notifications to the user while a file is downloading or when it finishes. You can even connect this with Flutter push notifications and Flutter in-app notifications for a better experience.

  • path_provider gives you safe folders to save your files on both Android and iOS devices.

  • android_path_provider helps you access Android-specific folders like the Downloads folder.

  • open_filex allows users to open the downloaded files directly from your app.

With these packages set up, you’re ready to start building the download feature step by step.

Installing Dependencies

Open your pubspec.yaml file and add the following dependencies under dependencies:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.9.0
  flutter_local_notifications: ^17.1.0
  path_provider: ^2.1.3
  android_path_provider: ^0.3.0
  open_filex: ^4.5.0

Then, run:

flutter pub get

This will install all the packages required for file downloading and notifications.

Required Permissions

Downloading files requires access to storage and notifications. Here’s how to set it up for each platform:

Android Permissions

Open android/app/src/main/AndroidManifest.xml and add these inside the <manifest> tag:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> 

These permissions allow:

  • Accessing the internet to download files
  • Reading/writing files to device storage
  • Showing download progress notifications

iOS Permissions

Open ios/Runner/Info.plist and add the following keys:

<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save downloaded files.</string>

<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>remote-notification</string>
</array>

Explanation:

  • NSPhotoLibraryAddUsageDescription: Required to save files in the user’s storage.
  • UIBackgroundModes: Ensures your app can download files and show notifications even when it’s running in the background.

Summary

By completing these steps, your Flutter project is now ready for file downloads. You have:

  • Installed all the necessary packages
  • Configured Android and iOS permissions correctly
  • Prepared your project to handle notifications and save files securely

Next, we’ll dive into writing the actual file download logic using Dio 5.9.0, along with progress tracking and notifications.

Building the File Downloader Logic

Creating the Download Function

Now that our project is set up, let’s create the function that will handle file downloads. We will use Dio 5.9.0 to fetch files and track progress.

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:android_path_provider/android_path_provider.dart';

final Dio dio = Dio();

Future downloadFile(String url, String fileName) async {
  String localPath = "";

  // Determine platform-specific path
  if (Platform.isAndroid) {
    try {
      localPath = await AndroidPathProvider.downloadsPath;
    } catch (e) {
      final directory = await getExternalStorageDirectory();
      localPath = directory?.path ?? "";
    }
  } else if (Platform.isIOS) {
    localPath = (await getApplicationDocumentsDirectory()).path;
  }

  try {
    // Download the file
    await dio.download(
      url,
      "$localPath/$fileName",
      onReceiveProgress: (received, total) {
        if (total != -1) {
          int progress = ((received / total) * 100).toInt();
          print("Download progress: $progress%");
        }
      },
    );

    print("File saved at $localPath/$fileName");
  } catch (e) {
    print("Download failed: $e");
  }
}

Handling onReceiveProgress for Download Percentage

The onReceiveProgress callback is triggered continuously as the file downloads. It provides received bytes and total bytes, which we can use to calculate progress:

onReceiveProgress: (received, total) {
  if (total != -1) {
    int progress = ((received / total) * 100).toInt();
    print("Download progress: $progress%");
  }
}

This helps us update progress bars or notifications in real-time.

Saving Files to Correct Directories

File storage differs between Android and iOS:

  • Android: Use the Downloads folder or external storage directories via android_path_provider.
  • iOS: Use the app’s Documents directory via path_provider.

Always check the path exists and handle cases where permission might be denied.

Handling Download Errors

Downloading files can fail for many reasons like network issues, invalid URLs, or permission errors. Dio throws DioException in such cases. Here’s how to handle them:

try {
  await dio.download(url, "$localPath/$fileName");
} on DioException catch (e) {
  if (e.type == DioExceptionType.connectionTimeout) {
    print("Connection timeout! Check your internet.");
  } else if (e.type == DioExceptionType.receiveTimeout) {
    print("Receive timeout! Slow network.");
  } else if (e.type == DioExceptionType.badResponse) {
    print("Server error: ${e.response?.statusCode}");
  } else {
    print("Unexpected error: $e");
  }
} catch (e) {
  print("General error: $e");
}

Using try-catch ensures your app doesn’t crash and provides feedback to users.

Summary

In this step, we learned how to:

  • Create a reusable download function using Dio
  • Track download progress with onReceiveProgress
  • Save files to the correct directories on Android and iOS
  • Handle errors effectively using DioException and try-catch

Next, we will integrate progress notifications so users can see real-time updates even when the app is in the background.

Adding Progress Notifications

Setting Up flutter_local_notifications

To show notifications during downloads, we will use flutter_local_notifications. First, initialize it in your app:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future initializeNotifications() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  const DarwinInitializationSettings initializationSettingsIOS =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

  const InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse response) async {
      // Handle notification tap
      print('Notification tapped with payload: ${response.payload}');
    },
  );
}

This sets up notifications for Android and iOS. Make sure you call initializeNotifications() in main() before running the app.

Showing Real-Time Download Progress

We can update notifications in real-time as the file downloads using the onReceiveProgress callback from Dio.

Future showProgressNotification(int progress) async {
  AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
    'download_channel',
    'Download Progress',
    channelShowBadge: false,
    importance: Importance.low,
    priority: Priority.low,
    ongoing: true,
    onlyAlertOnce: true,
    showProgress: true,
    maxProgress: 100,
    progress: progress,
  );

  NotificationDetails notificationDetails = NotificationDetails(
    android: androidDetails,
  );

  await flutterLocalNotificationsPlugin.show(
    0,
    "Downloading File...",
    "$progress% completed",
    notificationDetails,
  );
}

Call this function from onReceiveProgress to continuously update the notification.

Showing Download Completion Notification

Once the download is complete, we can show a new notification to indicate success:

Future showDownloadCompleteNotification(String fileName) async {
  AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
    'download_complete_channel',
    'Download Completed',
    importance: Importance.max,
    priority: Priority.high,
    autoCancel: true,
  );

  DarwinNotificationDetails iosDetails = const DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  NotificationDetails platformDetails = NotificationDetails(
    android: androidDetails,
    iOS: iosDetails,
  );

  await flutterLocalNotificationsPlugin.show(
    1,
    "Download Complete",
    fileName,
    platformDetails,
    payload: fileName,
  );
}

Open File on Notification Tap

We can open the downloaded file when the user taps on the notification using open_filex:

import 'package:open_filex/open_filex.dart';

void handleNotificationTap(String filePath) {
  OpenFilex.open(filePath);
}

In the initialization of flutter_local_notifications, use the onDidReceiveNotificationResponse callback to call this function:

await flutterLocalNotificationsPlugin.initialize(
  initializationSettings,
  onDidReceiveNotificationResponse: (NotificationResponse response) async {
    if (response.payload != null) {
      handleNotificationTap(response.payload!);
    }
  },
);

Summary

In this section, you learned how to:

  • Initialize flutter_local_notifications for Android and iOS
  • Show real-time download progress in notifications
  • Display a download completion notification
  • Open the downloaded file when the notification is tapped

Next, we will combine everything: download logic + notifications, and also add error handling to make a complete, user-friendly file downloader.

Bonus: Showing Download Progress in the App UI

Using ValueNotifier & ValueListenableBuilder

Besides notifications, it’s useful to show download progress directly in your app UI. We can use ValueNotifier and ValueListenableBuilder for this.

ValueNotifier downloadProgress = ValueNotifier(0);

// Update this inside Dio's onReceiveProgress
void updateProgress(int received, int total) {
  if (total != -1) {
    int progress = ((received / total) * 100).toInt();
    downloadProgress.value = progress;
  }
}

Now in your UI:

ValueListenableBuilder(
  valueListenable: downloadProgress,
  builder: (context, value, child) {
    return Column(
      children: [
        LinearProgressIndicator(
          value: value / 100,
          minHeight: 8,
        ),
        SizedBox(height: 8),
        Text("Download Progress: $value%"),
      ],
    );
  },
)

Displaying Percentage or Progress Bar

Using the example above, you can display either a LinearProgressIndicator or a simple text showing percentage. If you want, I have explained other amazing Flutter widgets as well. This gives users real-time feedback inside the app.

Advanced Tips & Best Practices

Canceling Downloads with CancelToken

Dio allows you to cancel downloads at any time using a CancelToken.

final CancelToken cancelToken = CancelToken();

// Start download
dio.download(
  url,
  filePath,
  cancelToken: cancelToken,
  onReceiveProgress: (received, total) {
    updateProgress(received, total);
  },
);

// Cancel download
cancelToken.cancel("User canceled the download");

Supporting Multiple Downloads Simultaneously

You can run multiple downloads in parallel by using separate Dio calls or Future.wait:

Future downloadMultipleFiles(List urls) async {
  await Future.wait(
    urls.map((url) => dio.download(
      url,
      "/path/to/save/${url.split('/').last}",
      onReceiveProgress: (received, total) {
        print("Progress for $url: ${(received / total * 100).toStringAsFixed(0)}%");
      },
    )),
  );
}

Retry Mechanisms & Error Logging

Sometimes downloads fail due to network issues. You can implement retries and log errors:

Future downloadWithRetry(String url, String filePath, {int retries = 3}) async {
  for (int i = 0; i < retries; i++) {
    try {
      await dio.download(url, filePath, onReceiveProgress: (received, total) {
        updateProgress(received, total);
      });
      print("Download succeeded!");
      break; // exit loop if successful
    } on DioException catch (e) {
      print("Download failed: ${e.message}");
      if (i == retries - 1) {
        print("All retries failed.");
      }
    }
  }
}

Summary

  • ValueNotifier & ValueListenableBuilder help display download progress inside the app.
  • LinearProgressIndicator or text percentage improves user feedback.
  • CancelToken allows stopping downloads anytime.
  • Support multiple parallel downloads easily with Future.wait.
  • Retry mechanism and error logging make your downloader robust.

With these tips, you can build a professional file downloader in Flutter that is user-friendly and reliable.

Conclusion

In this tutorial, we built a full-featured file downloader in Flutter using Dio 5.9.0 and flutter_local_notifications. Users can:

  • Download files with real-time progress tracking.
  • See notifications during download and on completion.
  • Open downloaded files directly from notifications.
  • View download progress inside the app using a progress bar or percentage.
  • Cancel downloads, retry failed attempts, and handle multiple downloads simultaneously.

This approach improves user experience by keeping users informed and engaged while downloads happen in the background. It also makes your app feel professional, reliable, and responsive.

Next Steps: Optimization and Enhancements

  • Implement background downloads that continue even if the app is closed.
  • Persist download history and show completed files in a library section.
  • Allow users to pause and resume downloads.
  • Support more file types and dynamic file naming conventions.
  • Improve error handling with custom messages and notifications.

FAQs

1. Can I download multiple files at the same time?

Yes! You can use Future.wait with multiple Dio download calls or create separate download functions for each file.

2. How can I cancel an ongoing download?

Use CancelToken provided by Dio. Call cancelToken.cancel() to stop the download at any time.

3. Will this work on both Android and iOS?

Yes, but make sure to request proper permissions and handle platform-specific paths for saving files.

4. Can I show progress inside the app UI?

Absolutely. Use ValueNotifier and ValueListenableBuilder to update a progress bar or percentage text in real-time.

5. How do I handle download errors?

Wrap your download logic in a try-catch block and catch DioException. You can retry downloads or log errors for debugging.

 

Leave a Reply

Your email address will not be published. Required fields are marked *