Building a Robust Video Download Feature in Flutter Using BLoC, Freezed, and Dio
In this guide, we’ll explore how to build a video download feature in Flutter using the BLoC pattern, Freezed for state management, Dio for HTTP requests, and dependency injection with Injectable. This tutorial will walk you through the entire process, from setting up the project to implementing the download logic and managing the download progress in real-time.
Table of Contents
- Introduction
- Project Setup
- Understanding the BLoC Pattern
- Setting Up Freezed for State Management
- Implementing the Download Logic with Dio
- Managing Download Progress with Callbacks
- Putting It All Together
- Conclusion
1. Introduction
When building modern Flutter applications, you often need to download files from the internet, such as videos, images, or documents. Handling file downloads efficiently requires managing the download progress, ensuring a responsive UI, and handling potential errors gracefully. In this tutorial, we’ll build a feature that downloads a video file and provides real-time progress updates to the user.
2. Project Setup
Start by adding the necessary dependencies to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
bloc: ^8.0.0
flutter_bloc: ^8.0.0
freezed_annotation: ^2.0.0
dartz: ^0.10.0
dio: ^5.0.0
path_provider: ^2.0.0
awesome_notifications: ^0.0.6+8
dev_dependencies:
build_runner: ^2.1.7
freezed: ^2.0.0
injectable_generator: ^1.5.2
These dependencies include:
- BLoC: For state management.
- Freezed: To create immutable classes for our states.
- Dio: A powerful HTTP client for making network requests.
- Injectable: For dependency injection.
- Path Provider: To manage file paths.
- Awesome Notifications: (Optional) For handling notifications during downloads.
After adding these dependencies, run flutter pub get
to install them.
3. Understanding the BLoC Pattern
The BLoC (Business Logic Component) pattern is a popular pattern in Flutter that helps separate business logic from UI. This separation makes the code more modular, testable, and scalable. In our case, the DownloadBloc
will manage the state of the download process.
BLoC Structure
- Events: Represent user actions or other triggers, like starting a download.
- States: Represent the current state of the application, like download progress or completion.
- BLoC: Takes events as input, processes them, and outputs new states.
4. Setting Up Freezed for State Management
Freezed helps in creating immutable classes for our states in a concise and readable way. Let’s define our DownloadState
using Freezed.
Defining the DownloadState
Create a download_state.dart
file and define the state as follows:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'download_state.freezed.dart';
@freezed
class DownloadState with _$DownloadState {
const factory DownloadState({
required String progressPercentage,
required bool isDownload,
required int progress,
required int total,
}) = _DownloadState;
factory DownloadState.Initial() =>
DownloadState(isDownload: false, progressPercentage: '0%', progress: 0, total: 0);
}
- Fields Explained:
progressPercentage
: A string representing the download progress in percentage.isDownload
: A boolean indicating whether a download is in progress.progress
: An integer representing the bytes downloaded so far.total
: An integer representing the total bytes of the file.- Initial State: The
Initial
factory method defines the initial state where no download has started (isDownload: false
) and the progress is set to0%
.
Generating the Freezed Code
After defining the state, run the following command to generate the necessary code:
flutter pub run build_runner build --delete-conflicting-outputs
This will generate the download_state.freezed.dart
file.
5. Implementing the Download Logic with Dio
We’ll now implement the actual file download logic using Dio, a powerful HTTP client that simplifies making requests and handling responses.
Abstracting the Download Service
Start by creating an abstract class DownloadServices
that defines the contract for the download functionality.
import 'package:dartz/dartz.dart';
abstract class DownloadServices {
Future downloadFile({
required String url,
required Function(int progress, int total) onProgress,
});
}
downloadFile: A method that accepts a URL and an onProgress
callback to monitor the download progress.
Implementing the Download Service
Now, create an implementation of DownloadServices
using Dio:
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:path_provider/path_provider.dart';
@LazySingleton(as: DownloadServices)
class DownloadImpl extends DownloadServices {
@override
Future downloadFile({
required String url,
required Function(int progress, int total) onProgress,
}) async {
final dio = Dio();
try {
Directory directory = Directory('/storage/emulated/0/Download');
final filePath = "${directory.path}/MyVideo.mp4";
log(filePath.toString());
await dio.download(
url,
filePath,
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total * 100).toInt();
onProgress(received, total);
}
},
);
} catch (e) {
log('Failed to download video: $e');
throw Exception('Failed to download video: $e');
}
}
}
- Dio Initialization: A
Dio
instance is created to handle HTTP requests. - Directory Path: We define the directory path where the video will be saved.
- File Download: The
download
method of Dio handles the file download. TheonReceiveProgress
callback is used to monitor the download progress and update the UI.
6. Managing Download Progress with Callbacks
The onProgress
callback is crucial for updating the UI with real-time progress. Let’s break down how this works.
Understanding the onProgress Callback
The onProgress
function in downloadFile
receives two parameters: progress
and total
.
Future downloadFile({
required String url,
required Function(int progress, int total) onProgress,
});
- progress: The number of bytes downloaded so far.
- total: The total number of bytes of the file being downloaded.
This callback allows you to calculate the download percentage and update the state accordingly.
7. Putting It All Together
Now that we have the core components ready, let’s create the DownloadBloc
and connect everything.
Implementing the DownloadBloc
Create a download_bloc.dart
file and define the BLoC:
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc_full/domain/download/download_services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
part 'download_event.dart';
part 'download_state.dart';
part 'download_bloc.freezed.dart';
@injectable
class DownloadBloc extends Bloc<DownloadEvent, DownloadState> {
DownloadServices downloadServices;
DownloadBloc(this.downloadServices) : super(DownloadState.Initial()) {
on<_StartDownload>((event, emit) async {
await downloadServices.downloadFile(
url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
onProgress: (progress, total) {
int progressPercentage = ((progress / total) * 100).toInt();
emit(state.copyWith(
isDownload: true,
progressPercentage: '${progressPercentage}%',
progress: progress,
total: total));
},
);
});
}
}
- BLoC Constructor: The BLoC takes an instance of
DownloadServices
to handle the actual download logic. - Event Handling: When the
_StartDownload
event is triggered, thedownloadFile
method is called, and the progress is updated using theemit
function.
8. UI-Flutter
class DownloadScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Download Progress")),
body: Column(
children: [
BlocConsumer<DownloadBloc, DownloadState>(
listener: (context, state) {
// Manage side effects here
},
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: state.total > 0
? state.progress / state.total
: 0,
),
),
SizedBox(width: 12),
Text("${state.progressPerstage}")
],
),
);
},
),
ElevatedButton(
onPressed: () async {
context.read<DownloadBloc>().add(DownloadEvent.startDownload());
},
child: Text("Download"),
),
],
),
);
}
}
9. Conclusion
In this tutorial, we built a video download feature in Flutter using the BLoC pattern, Freezed for state management, and Dio for handling the HTTP requests. We covered how to manage download progress and update the UI in real-time. This implementation is a robust foundation for handling file downloads in Flutter, and you can further enhance it by adding more features like pause/resume, notifications, or background downloads.
By following this guide, you’ve learned not only how to implement a download feature but also how to structure your Flutter application using best practices for state management and dependency injection. Feel free to experiment and expand upon this implementation to fit your application’s needs.
If you have any questions, comments, or suggestions, feel free to share them below. Happy coding!