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
- Introduction
- Why Efficient File Downloads Matter in Flutter Apps
- 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
- Setting Up Your Flutter Project for File Downloads
- Required Packages
- Installing Dependencies
- Required Permissions
- Building the File Downloader Logic
- Creating the Download Function
- Saving Files to Correct Directories
- Handling Download Errors
- Adding Progress Notifications
- Setting Up flutter_local_notifications
- Showing Real-Time Download Progress
- Showing Download Completion Notification
- Bonus: Showing Download Progress in the App UI
- Using ValueNotifier & ValueListenableBuilder
- Advanced Tips & Best Practices
- 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:
- Interceptors: Log requests/responses, add authentication tokens, or handle errors in one place.
- Timeouts: Set custom connection and response timeouts easily.
- Cancellation: Stop long-running requests with
CancelToken. - File handling: Download and upload files with progress tracking.
- 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
DioExceptionwith status codes, messages, and stack traces. - Interceptors: Easily modify requests or responses globally without changing each call.
- File download support: Supports
ResponseType.bytesor 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:
- Updated handling for
ResponseType.bytesand streamed downloads - Better
DioExceptionmessages for debugging network errors - Improved interceptors system for sequential and queued requests
- Enhanced support for canceling requests and tracking progress
- 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-catchblocks. - 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.bytesto download binary data like PDFs, images, or videos. - Track progress using
onReceiveProgress, which provides the number of bytes downloaded and total bytes. - Use
DioExceptionto 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.bytesensures the file is downloaded as binary data.onReceiveProgressprints 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 getThis 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
DioExceptionandtry-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.

