Article original : How to Build an Upload Service in Flutter Web with Firebase
L'upload de fichiers est l'une des exigences les plus courantes dans les applications web modernes. Qu'il s'agisse de photos de profil, de documents ou d'uploads groupés, les utilisateurs attendent une expérience fluide et fiable. Avec Flutter Web et Firebase Storage, vous pouvez implémenter cette fonctionnalité de manière propre et évolutive.
Dans cet article, vous apprendrez à créer un service d'upload réutilisable qui :
Télécharge des fichiers uniques et multiples vers Firebase Storage
Retourne les URLs de téléchargement des fichiers
Utilise l'Injection de Dépendances (DI) avec
injectablepour garder le code modulaire, testable et facile à maintenir
À la fin, vous disposerez d'un service d'upload prêt pour la production pour votre projet Flutter Web.
Table des matières :
Pourquoi l'upload de fichiers est important dans Flutter Web
Comment définir le modèle de données d'upload et l'interface du service
Pourquoi l'upload de fichiers est important dans Flutter Web
Lors du développement pour le web, les utilisateurs attendent souvent des fonctionnalités telles que l'upload d'une photo de profil, la soumission de documents ou le partage de médias. Contrairement au mobile, l'environnement web nécessite la manipulation des fichiers via les APIs du navigateur, qui doivent ensuite être intégrées à des services backend comme Firebase pour la persistance.
Aperçu du flux d'upload
Voici un aperçu de haut niveau du fonctionnement du processus d'upload :
L'utilisateur sélectionne un fichier ou une image à l'aide d'un sélecteur de fichiers du navigateur.
Flutter lit le fichier sous forme de
Uint8List.Le fichier est uploadé vers Firebase Storage.
Une URL de téléchargement est générée et stockée dans Firestore (ou utilisée directement).
Prérequis
Avant de commencer, assurez-vous d'avoir les éléments suivants :
Un projet Flutter Web
flutter config --enable-web flutter create my_web_project cd my_web_projectFirebase configuré dans votre application Flutter : Suivez Ajouter Firebase à votre application Flutter (Web) et incluez le snippet du SDK Firebase dans
index.html.Firebase Storage activé dans la Console Firebase : Allez dans Build > Storage > Get Started et autorisez l'accès en lecture/écriture pour les tests. Exemple de règles :
service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if true; } } }N'utilisez pas ces règles en production.
Dépendances requises dans votre
pubspec.yaml:dependencies: firebase_core: ^3.13.0 firebase_storage: ^12.4.2 injectable: ^2.3.2 get_it: ^8.0.3 dev_dependencies: build_runner: ^2.4.13 injectable_generator: ^2.4.1Lancez
flutter pub getpour les installer.
Comment définir le modèle de données d'upload et l'interface du service
Nous commençons par un modèle de données pour représenter le fichier et une interface de service pour définir le contrat d'upload.
import 'dart:typed_data';
class UploadData {
final Uint8List fileData; // Fichier au format binaire
final String folderName; // Chemin du dossier dans Firebase Storage
final String fileName; // Nom du fichier
const UploadData({
required this.fileData,
required this.fileName,
required this.folderName,
});
}
Ensuite, créez un service abstrait qui définit ce que la logique d'upload doit faire.
abstract class IUploadService {
Future<String> uploadDoc({
required UploadData file,
});
Future<List<String>> uploadMultipleDoc({
required List<UploadData> files,
});
}
Voici ce qui se passe dans ce code :
uploadDoc: Upload un fichier et retourne son URL de téléchargementuploadMultipleDoc: Upload plusieurs fichiers en parallèle et retourne une liste d'URLs

Comment implémenter le service d'upload
Implémentons maintenant la logique d'upload avec Firebase Storage.
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';
@LazySingleton(as: IUploadService)
class UploadService extends IUploadService {
final FirebaseStorage firebaseStorage;
UploadService({required this.firebaseStorage});
@override
Future<String> uploadDoc({required UploadData file}) async {
try {
var storageRef = firebaseStorage.ref('${file.folderName}/${file.fileName}');
var uploadTask = storageRef.putData(file.fileData);
TaskSnapshot snapshot = await uploadTask;
return await snapshot.ref.getDownloadURL();
} on FirebaseException catch (e) {
throw CustomError(
errorMsg: "Échec de l'upload Firebase : ${e.message}",
code: e.code,
plugin: e.plugin,
);
} catch (e) {
if (kDebugMode) print("Erreur inattendue : $e");
rethrow;
}
}
@override
Future<List<String>> uploadMultipleDoc({required List<UploadData> files}) async {
return await Future.wait(
files.map((file) => uploadDoc(file: file)),
);
}
}
Ce code définit une classe de service pour l'upload de documents vers Firebase Storage dans une application Flutter. Décomposons-le étape par étape :
1. Imports
firebase_storage: fournit le SDK Firebase Storage pour uploader et gérer les fichiers.flutter/foundation.dart: donne accès à des constantes commekDebugModepour les logs de débogage.injectable.dart: permet l'injection de dépendances en utilisant le package injectable + getIt.i_upload_service.dart: définit le contrat/interface abstrait pour le service d'upload.custom_error.dart: définit une classe d'erreur personnalisée pour standardiser la gestion des erreurs.
2. Configuration de l'injection de dépendances
@LazySingleton(as: IUploadService)
class UploadService extends IUploadService {
@LazySingleton(as: IUploadService)enregistreUploadServicecomme l'implémentation deIUploadService.Cela signifie que partout dans l'application où
IUploadServiceest requis, getIt fournira une instance deUploadService.C'est un singleton, donc une seule instance est créée et réutilisée dans toute l'application.
3. Constructeur
final FirebaseStorage firebaseStorage;
UploadService({required this.firebaseStorage});
La classe nécessite une instance
FirebaseStorage, qui sera également injectée automatiquement.Cela rend le service plus facile à tester et à remplacer.
4. Upload d'un fichier unique
@override
Future<String> uploadDoc({required UploadData file}) async {
try {
var storageRef = firebaseStorage.ref('${file.folderName}/${file.fileName}');
var uploadTask = storageRef.putData(file.fileData);
TaskSnapshot snapshot = await uploadTask;
return await snapshot.ref.getDownloadURL();
} on FirebaseException catch (e) {
throw CustomError(
errorMsg: "Échec de l'upload Firebase : ${e.message}",
code: e.code,
plugin: e.plugin,
);
} catch (e) {
if (kDebugMode) print("Erreur inattendue : $e");
rethrow;
}
}
Ce que fait ce code :
Crée une référence dans Firebase Storage au chemin
folderName/fileName.Upload les octets bruts du fichier (
file.fileData) en utilisantputData.Attend que l'upload soit terminé et récupère un
TaskSnapshot.À partir du snapshot, obtient l'URL de téléchargement du fichier uploadé et la retourne.
Si une
FirebaseExceptionsurvient, il encapsule l'erreur dans unCustomErrorpersonnalisé.Toute autre erreur inattendue est loggée (uniquement en mode debug) et relancée.
5. Upload de fichiers multiples
@override
Future<List<String>> uploadMultipleDoc({required List<UploadData> files}) async {
return await Future.wait(
files.map((file) => uploadDoc(file: file)),
);
}
Ce que fait le code :
Accepte une liste d'objets
UploadData.Pour chaque fichier, il appelle
uploadDoc(la fonction d'upload unique).Future.waitexécute tous les uploads en parallèle, attend qu'ils se terminent et retourne une liste d'URLs de téléchargement.
Cette classe est un service d'upload Firebase Storage. Elle peut uploader des documents uniques ou multiples. Elle suit les principes d'injection de dépendances pour la testabilité et l'évolutivité. Elle utilise une gestion d'erreurs avec CustomError pour fournir des messages d'erreur plus clairs. Les uploads multiples sont exécutés en parallèle par souci d'efficacité.

Comment gérer les erreurs
Au lieu de s'appuyer sur des instructions print brutes, il est préférable d'utiliser une classe d'erreur structurée. Une classe d'erreur structurée organise toutes les informations relatives à l'erreur, comme le message, le code et la source, dans un seul objet. Cela rend la gestion des erreurs cohérente, réutilisable et facile à gérer. Vous pouvez inspecter, logger ou afficher les erreurs par programmation, ce qui est beaucoup plus maintenable que des prints dispersés.
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)';
}
}
Pourquoi devriez-vous utiliser cette approche :
Assure la cohérence dans tout le projet.
Rend les erreurs réutilisables n'importe où dans l'application.
Permet une gestion programmatique (par exemple, agir différemment selon le code d'erreur).
Fournit des informations de débogage claires via
toString().Évolue bien à mesure que votre application grandit.
Injection de dépendances avec injectable
Dans une application classique, vous pourriez créer manuellement des instances de service comme UploadService ou FirebaseStorage partout où vous en avez besoin. Mais à mesure que votre application grandit, la création et la transmission manuelles des dépendances deviennent désordonnées, sujettes aux erreurs et difficiles à tester.
C'est là qu'intervient l'Injection de Dépendances (DI). La DI vous permet de déclarer les dépendances une seule fois et de laisser un framework s'occuper de les créer et de les fournir partout où elles sont nécessaires. Le package injectable dans Flutter fonctionne avec getIt pour automatiser ce processus.
Au lieu de créer UploadService manuellement, vous le configurez avec injectable pour que votre application obtienne automatiquement la bonne instance en cas de besoin, selon les patterns singleton ou lazy-loading.
Étape 1 : Annotez votre service
@LazySingleton(as: IUploadService)
class UploadService implements IUploadService {
// votre logique d'upload ici
}
@LazySingleton(as: IUploadService) indique à injectable :
Lazy : Ne créer l'instance que lors de sa première utilisation.
Singleton : Réutiliser la même instance dans toute l'application.
as: IUploadService : Exposer le service via son interface, facilitant les tests et l'échange d'implémentations.
Étape 2 : Lancez le générateur
flutter pub run build_runner build
Cette commande génère le code qui relie toutes vos dépendances injectables entre elles, de sorte que vous n'ayez pas à les instancier manuellement.
Étape 3 : Créez un module injectable
import 'package:firebase_storage/firebase_storage.dart';
import 'package:injectable/injectable.dart';
@module
abstract class InjectableModule {
@lazySingleton
FirebaseStorage get firebaseStorage => FirebaseStorage.instance;
}
Ce code configure l'injection de dépendances pour FirebaseStorage en utilisant le package injectable. Laissez-moi vous expliquer :
@module: L'annotation@moduleindique àinjectableque cette classe servira de fournisseur de dépendances externes (des choses que vous ne créez pas manuellement mais que vous obtenez de bibliothèques, de SDKs ou d'APIs).Dans ce cas,
FirebaseStorageprovient du SDK Firebase, vous ne le construisez donc pas vous-même. Vous obtenez simplement une instance du SDK.abstract class InjectableModule: Il s'agit d'une classe de module spéciale qui contient des définitions de dépendances. Comme elle est abstraite, elle ne sera pas instanciée directement. Au lieu de cela,injectablegénère le code pour gérer l'injection.@lazySingleton: Cette annotation indique àinjectableque la dépendance doit être créée une seule fois et réutilisée dans toute l'application (pattern singleton).Lazy signifie qu'elle ne sera pas créée tant qu'elle n'est pas réellement nécessaire.
Singleton signifie que la même instance sera réutilisée partout après la première création.
FirebaseStorage get firebaseStorage => FirebaseStorage.instance;: Cette ligne définit quelle dépendance fournir. Ici, elle dit :Chaque fois que quelque chose dans l'application a besoin d'une instance
FirebaseStorage, injectezFirebaseStorage.instance.De cette façon, vous ne créez pas et ne passez pas manuellement
FirebaseStoragevous-même –injectableplusgetIts'en chargent automatiquement.
En pratique, cela garantit que partout dans votre application où vous avez besoin de FirebaseStorage, vous pouvez simplement l'injecter via l'injection par constructeur (par exemple, dans votre UploadService) sans l'instancier manuellement.
Étape 4 : Résoudre le service n'importe où
final uploadService = getIt<IUploadService>();
Pourquoi nous faisons cela
En utilisant injectable :
Vous arrêtez d'instancier manuellement les dépendances partout.
Vos services sont plus faciles à tester, car vous pouvez échanger les implémentations via les interfaces.
Vous garantissez les patterns singleton et le lazy loading sans code répétitif supplémentaire.
Votre application devient plus maintenable, surtout à mesure qu'elle grandit.
En pratique :
Partout dans votre application où UploadService a besoin de FirebaseStorage, vous l'injectez simplement via le constructeur :
class UploadService implements IUploadService {
final FirebaseStorage _firebaseStorage;
UploadService(this._firebaseStorage);
// Utilisez _firebaseStorage ici
}
Injectable + getIt se charge de fournir automatiquement la bonne instance de _firebaseStorage.

Comment utiliser le service d'upload
Le Service d'Upload est un service modulaire et réutilisable dans votre application qui gère l'upload de fichiers vers Firebase Storage. En utilisant ce service, vous faites abstraction des interactions directes avec Firebase, vous gardez votre code propre et vous exploitez l'injection de dépendances pour accéder au service n'importe où dans votre application.
Le Service d'Upload offre plusieurs options :
Upload de fichier unique – Upload d'un fichier à la fois et obtention de son URL de téléchargement.
Upload de fichiers multiples – Upload d'un lot de fichiers en une seule fois et réception d'une liste d'URLs de téléchargement.
Gestion des erreurs – Tout problème lors de l'upload (comme des erreurs réseau ou des problèmes de permission) est capturé et peut être géré avec élégance.
Ci-dessous, nous allons passer en revue étape par étape comment utiliser ces options en pratique.
Exemple : Uploader un fichier unique.
Future<void> uploadFile(Uint8List fileData) async {
final file = UploadData(
fileData: fileData,
fileName: 'example.txt',
folderName: 'documents',
);
try {
final uploadService = getIt<IUploadService>();
final downloadUrl = await uploadService.uploadDoc(file: file);
print('Upload réussi : $downloadUrl');
} catch (e) {
print('Échec de l'upload : $e');
}
}
Cette fonction uploadFile est une enveloppe qui prépare un fichier pour l'upload et délègue l'upload réel à votre UploadService via l'injection de dépendances. Laissez-moi vous expliquer cela étape par étape :
Future<void> uploadFile(Uint8List fileData) async {
final file = UploadData(
fileData: fileData,
fileName: 'example.txt',
folderName: 'documents',
);
Tout d'abord, elle prend un fichier sous forme d'octets bruts (
Uint8List fileData).Ensuite, elle encapsule ces données dans un objet
UploadData, en lui donnant unfileName(example.txt) et unfolderName(documents). Cela crée essentiellement des métadonnées sur le fichier, afin que votre service d'upload sache comment l'appeler et où le stocker dans Firebase Storage.
try {
final uploadService = getIt<IUploadService>();
final downloadUrl = await uploadService.uploadDoc(file: file);
print('Upload réussi : $downloadUrl');
} catch (e) {
print('Échec de l'upload : $e');
}
}
Ensuite, elle récupère l'instance
IUploadServiceen utilisantgetIt(votre conteneur d'injection de dépendances). Grâce à la liaison que vous avez définie plus tôt (UploadServiceenregistré en tant queIUploadService),getItsait vous donner la bonne implémentation.Elle appelle
uploadService.uploadDoc(file: file)qui déclenche l'upload réel vers Firebase Storage. En cas de succès, Firebase renvoie une URL de téléchargement du fichier uploadé.La fonction affiche ensuite :
"Upload réussi : <downloadUrl>"si l'upload a fonctionné."Échec de l'upload : <erreur>"si une erreur est survenue (par exemple, pas d'internet ou problèmes de permission Firebase).
En termes simples :
Entrée : Données brutes du fichier (octets).
Processus : Encapsulation dans un objet
UploadData→ envoi à Firebase viaUploadService.Sortie : Affiche l'URL publique de téléchargement si l'upload réussit, ou affiche un message d'erreur s'il échoue.
Exemple : Uploader plusieurs fichiers.
Future<void> uploadMultiple(List<Uint8List> filesData) async {
final uploadService = getIt<IUploadService>();
final files = filesData.map((data) => UploadData(
fileData: data,
fileName: '${DateTime.now().millisecondsSinceEpoch}.txt',
folderName: 'batch_docs',
)).toList();
try {
final urls = await uploadService.uploadMultipleDoc(files: files);
print('Tous les fichiers ont été uploadés : $urls');
} catch (e) {
print('Échec de l'upload groupé : $e');
}
}
Cette fonction gère l'upload groupé de plusieurs fichiers vers Firebase Storage en utilisant le IUploadService. Décomposons-la étape par étape :
1. Accéder au service d'upload
final uploadService = getIt<IUploadService>();
Ici, getIt récupère l'instance IUploadService enregistrée via l'injection de dépendances. Ce service masque toute la logique d'upload des fichiers, de sorte que vous ne manipulez pas directement les APIs Firebase dans cette méthode.
2. Préparer la liste des fichiers
final files = filesData.map((data) => UploadData(
fileData: data,
fileName: '${DateTime.now().millisecondsSinceEpoch}.txt',
folderName: 'batch_docs',
)).toList();
filesData est une liste de contenus de fichiers bruts (Uint8List). Pour chaque fichier de la liste, elle crée un objet UploadData.
Le nom du fichier est généré dynamiquement en utilisant l'horodatage actuel (DateTime.now().millisecondsSinceEpoch), garantissant que chaque fichier a un nom unique.
Tous les fichiers sont placés dans le dossier "batch_docs" de Firebase Storage. De cette façon, vous disposez d'une liste structurée de fichiers prêts à être uploadés.
3. Mécanisme d'upload de fichiers multiples
final urls = await uploadService.uploadMultipleDoc(files: files);
On demande au uploadService d'uploader tous les fichiers d'un coup en utilisant sa méthode uploadMultipleDoc. Il uploade chaque fichier vers Firebase Storage. Une fois terminé, il retourne une liste d'URLs de téléchargement, une pour chaque fichier uploadé.
4. Gérer le succès ou l'échec
print('Tous les fichiers ont été uploadés : $urls');
En cas de succès, elle affiche les URLs de tous les fichiers uploadés (afin que vous puissiez les utiliser plus tard, par exemple, pour afficher ou partager les documents).
print('Échec de l'upload groupé : $e');
Si quelque chose ne va pas, elle capture l'exception et logge le message d'erreur.
En résumé, cette fonction prend plusieurs fichiers bruts, les encapsule dans des objets UploadData, les uploade tous vers Firebase Storage en utilisant la couche de service, et affiche les URLs de téléchargement résultantes.
Bonnes pratiques
Validez la taille du fichier avant l'upload pour éviter les fichiers trop volumineux.
Restreignez les types de fichiers (par exemple, uniquement
image/*) pour améliorer la sécurité.Stockez les métadonnées (comme l'ID utilisateur, l'horodatage) dans Firestore avec l'URL de téléchargement.
Utilisez des chemins uniques (
uploads/userId/filename) pour éviter les collisions.
Conclusion
Vous disposez maintenant d'un service d'upload réutilisable et modulaire pour Flutter Web qui prend en charge l'upload de fichiers uniques et multiples, gère les erreurs de manière structurée et utilise l'Injection de Dépendances pour une architecture propre.
Cette base facilite l'extension future du service, par exemple en ajoutant la suppression de fichiers, le suivi de la progression de l'upload ou les uploads authentifiés.