Building a Robust Video Download Feature in Flutter Using BLoC, Freezed, and Dio

Mohammed shamseer pv
5 min readAug 30, 2024

--

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

  1. Introduction
  2. Project Setup
  3. Understanding the BLoC Pattern
  4. Setting Up Freezed for State Management
  5. Implementing the Download Logic with Dio
  6. Managing Download Progress with Callbacks
  7. Putting It All Together
  8. 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 to 0%.

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. The onReceiveProgress 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, the downloadFile method is called, and the progress is updated using the emit 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!

--

--

Mohammed shamseer pv
Mohammed shamseer pv

Written by Mohammed shamseer pv

skilled in Flutter, Node.js, Python, and Arduino, passionate about AI and creating innovative solutions. Active in tech community projects.

No responses yet