Building an Upload Service for Flutter Web Using Firebase Storage
A simple Upload Service for Single and Multiple File Uploads Using Firebase Storage
Uploading files in a Flutter web application can be a powerful feature, especially when integrated with cloud storage like Firebase. This article will guide you through building an upload service using Firebase Storage for Flutter Web for single and multiple file uploads, allowing you to manage file uploads efficiently. We'll also implement Dependency Injection (DI) using injectable
to make our codebase more modular and maintainable.
Overview
In this article, we'll cover the following steps:
Setting up Firebase Storage for file uploads.
Creating a service interface for uploading single and multiple files.
Implementing the upload functionality using Firebase Storage.
Utilizing
injectable
for Dependency Injection (DI) to decouple the upload service from the rest of the app.
Prerequisites
Before we begin, ensure that you have:
A Flutter project configured for web.
Firebase set up in your Flutter project, including Firebase Storage. You can refer to the official Firebase documentation for setup instructions.
The
firebase_storage
package added to yourpubspec.yaml
.
1. Defining the Upload Service Interface
First, we’ll define the data model and service interface for our upload functionality. This interface ensures that the implementation can be easily replaced or extended later.
Here’s the UploadData
class, which represents the file data that will be uploaded to Firebase:
import 'dart:typed_data';
class UploadData {
final Uint8List fileData; // The actual file in binary format
final String folderName; // Folder in Firebase Storage where the file will be stored
final String fileName; // Name of the file
const UploadData({
required this.fileData,
required this.fileName,
required this.folderName,
});
}
We also create an abstract class IUploadService
to define the contract for uploading files:
abstract class IUploadService {
Future<String> uploadDoc({
required UploadData file,
});
Future<List<String>> uploadMultipleDoc({
required List<UploadData> files,
});
}
This interface defines two main functions:
uploadDoc
: Uploads a single file to Firebase and returns the download URL.uploadMultipleDoc
: Uploads multiple files in parallel and returns a list of download URLs.
2. Implementing the Upload Service
Now that we have our interface and data model, let's implement the actual upload service. The service will handle interactions with Firebase Storage to upload documents and retrieve their download URLs.
Here is the implementation of the UploadService
class:
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'i_upload_service.dart';
import 'custom_error.dart'; // Custom error handling class
@LazySingleton(as: IUploadService)
class UploadService extends IUploadService {
final FirebaseStorage firebaseStorage;
UploadService({
required this.firebaseStorage,
});
@override
Future<String> uploadDoc({
required UploadData file,
}) async {
try {
// Create a reference to the file location in Firebase Storage
var storageRef = firebaseStorage.ref('${file.folderName}/${file.fileName}');
var uploadTask = storageRef.putData(file.fileData); // Upload the file
TaskSnapshot snapshot = await uploadTask; // Wait for the upload to complete
// Return the download URL
return await snapshot.ref.getDownloadURL();
} on FirebaseException catch (e) {
// Handle Firebase-specific errors
throw CustomError(
errorMsg: "An error occurred! ($e)",
code: e.code,
plugin: e.plugin,
);
} catch (e) {
// Catch any other error
if (kDebugMode) {
print('Error: $e');
}
rethrow;
}
}
@override
Future<List<String>> uploadMultipleDoc({
required List<UploadData> files,
}) async {
// Use Future.wait to upload all files in parallel
return await Future.wait(files.map((file) => uploadDoc(file: file)));
}
}
3. Error Handling
In the UploadService
, we handle errors using a CustomError
class. This class helps us capture more specific information about errors, especially those from Firebase:
import 'package:equatable/equatable.dart';
class CustomError extends Equatable {
final String errorMsg;
final String code;
final String plugin;
const CustomError({
required this.errorMsg,
required this.code,
required this.plugin,
});
@override
List<Object?> get props => [errorMsg, code, plugin];
@override
String toString() {
return 'CustomError{errorMsg: $errorMsg, code: $code, plugin: $plugin}';
}
}
This class provides a structured way to throw and handle errors throughout the upload process.
4. Injecting Dependencies with injectable
To maintain clean code architecture and ensure loose coupling between components, we use the injectable
package for Dependency Injection (DI). DI allows us to inject FirebaseStorage
into the UploadService
class, making the service easier to mock or replace during testing or future updates.
Here’s how you can set up injectable
in your project:
- Add
injectable
andget_it
packages to yourpubspec.yaml
:
dependencies:
injectable: ^2.3.2
get_it: ^7.6.7
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
injectable_generator: ^2.4.1
- Annotate the
UploadService
class with@LazySingleton
to register it with the DI container:
@LazySingleton(as: IUploadService)
class UploadService extends IUploadService {
// constructor and methods
}
- Configure
injectable
to handle the DI logic for you. Run theinjectable
generator:
flutter pub run build_runner build
With injectable
, the UploadService
is automatically injected wherever needed, ensuring that the FirebaseStorage
instance is provided without manually creating it in every part of the app.
5. Using the Upload Service
To use the UploadService
in your Flutter app, you can simply call its methods after injecting it via get_it
or directly if you're not using DI for that part yet.
Here’s an example of how you could use the service to upload a single file:
final uploadService = getIt<IUploadService>();
Future<void> uploadFile(Uint8List fileData) async {
final file = UploadData(
fileData: fileData,
fileName: 'example.txt',
folderName: 'documents',
);
try {
final downloadUrl = await uploadService.uploadDoc(file: file);
print('File uploaded successfully: $downloadUrl');
} catch (e) {
print('Upload failed: $e');
}
}
By implementing an upload service using Firebase Storage and injectable
, you’ve created a scalable and modular way to manage file uploads in your Flutter web app. This setup also ensures that your code is easily testable and maintainable. You can extend this service further to handle other types of file operations, such as deleting files or tracking upload progress.
With Dependency Injection through injectable
, your upload service remains loosely coupled with other parts of the app, making it easier to maintain and upgrade over time.
To delve more into this here are some helpful links to the official documentation related to Firebase Storage and Dependency Injection in Flutter:
Firebase Storage for Flutter:
Dependency Injection in Flutter with
injectable
: