Firebase Storage avec Flutter
Sommaire
- 1- Objectif
- 2- Présentation de FireBase Storage
- 2.1- Qu'est-ce que Firebase Storage?
- 2.1.1- Définition
- 2.1.2- Caractéristiques Principales
- 2.2- Architecture du Projet
- 3- Configuration initiale
- 3.1- Ajout des Dépendances
- 3.2- Configuration des Permissions
- 3.2.1- Android
- 3.2.2- iOS
- 4- Fichiers à réaliser
- 4.1- Modèle de données
- 4.2- Widget de Sélection d'Image
- 4.3- Page de Gestion du Stockage
- 4.4- Intégration finale
- 5- Configuration Security Rules
- 6- Configuration Security Rules
- 6.1.1- Cours Flutter
Firebase Storage avec Flutter

-
Objectif
- Maîtriser Firebase Storage pour le stockage de fichiers
- Implémenter le téléversement d’images
- Afficher et gérer les images stockées
- Gérer la suppression et l’organisation des fichiers
-
Présentation de FireBase Storage
-
Qu’est-ce que Firebase Storage?
-
Définition
- Firebase Storage est un service de stockage d’objets qui permet de stocker et servir du contenu généré par les utilisateurs (images, vidéos, audio, documents).
-
Caractéristiques Principales
- Stockage sécurisé et évolutif
- Upload et download asynchrones
- Gestion des métadonnées
- Contrôle d’accès via Security Rules
- Intégration avec Cloud Functions
-
Architecture du Projet
-
Configuration initiale
-
Ajout des Dépendances
- Commande terminal :flutter pub get
-
Configuration des Permissions
-
Android
-
iOS
-
Fichiers à réaliser
-
Modèle de données
-
Widget de Sélection d’Image
-
Page de Gestion du Stockage
-
Intégration finale
-
Configuration Security Rules
-
Configuration Security Rules
-
lib/
├── models/
│ └── stored_file.dart
├── services/
│ └── storage_service.dart
├── widgets/
│ └── image_upload_widget.dart
└── pages/
└── storage_page.dart
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.0
firebase_storage: ^11.5.4
image_picker: ^1.0.4
cached_network_image: ^3.3.0
permission_handler: ^11.0.1
file_picker: ^6.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
<!-- 📁 android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Pour Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 📁 ios/Runner/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>Pour prendre des photos à uploader</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Pour sélectionner des photos depuis la galerie</string>
<key>NSMicrophoneUsageDescription</key>
<string>Pour enregistrer de l'audio avec les vidéos</string>
stored_file.dart
// 📁 lib/models/stored_file.dart
class StoredFile {
final String id;
final String name;
final String url;
final String path;
final DateTime uploadDate;
final int size;
final String? contentType;
StoredFile({
required this.id,
required this.name,
required this.url,
required this.path,
required this.uploadDate,
required this.size,
this.contentType,
});
factory StoredFile.fromMap(Map map) {
return StoredFile(
id: map['id'] ?? '',
name: map['name'] ?? '',
url: map['url'] ?? '',
path: map['path'] ?? '',
uploadDate: DateTime.parse(map['uploadDate']),
size: map['size'] ?? 0,
contentType: map['contentType'],
);
}
Map toMap() {
return {
'id': id,
'name': name,
'url': url,
'path': path,
'uploadDate': uploadDate.toIso8601String(),
'size': size,
'contentType': contentType,
};
}
}
storage_page.dart
// 📁 lib/widgets/image_upload_widget.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class ImageUploadWidget extends StatefulWidget {
final Function(XFile) onImageSelected;
final String? initialImageUrl;
const ImageUploadWidget({
Key? key,
required this.onImageSelected,
this.initialImageUrl,
}) : super(key: key);
@override
_ImageUploadWidgetState createState() => _ImageUploadWidgetState();
}
class _ImageUploadWidgetState extends State {
XFile? _selectedImage;
String? _imageUrl;
final ImagePicker _picker = ImagePicker();
@override
void initState() {
super.initState();
_imageUrl = widget.initialImageUrl;
}
// 📸 Prendre une photo avec la caméra
Future _takePhoto() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (image != null) {
setState(() {
_selectedImage = image;
_imageUrl = null;
});
widget.onImageSelected(image);
}
} catch (e) {
_showErrorSnackbar('Erreur caméra: $e');
}
}
// 🖼️ Choisir depuis la galerie
Future _pickFromGallery() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (image != null) {
setState(() {
_selectedImage = image;
_imageUrl = null;
});
widget.onImageSelected(image);
}
} catch (e) {
_showErrorSnackbar('Erreur galerie: $e');
}
}
void _showErrorSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
void _clearSelection() {
setState(() {
_selectedImage = null;
_imageUrl = null;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Aperçu de l'image
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(12),
),
child: _buildImagePreview(),
),
SizedBox(height: 16),
// Boutons d'action
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _takePhoto,
icon: Icon(Icons.camera_alt),
label: Text('Caméra'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed: _pickFromGallery,
icon: Icon(Icons.photo_library),
label: Text('Galerie'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
if (_selectedImage != null || _imageUrl != null)
IconButton(
onPressed: _clearSelection,
icon: Icon(Icons.delete, color: Colors.red),
tooltip: 'Supprimer',
),
],
),
],
);
}
Widget _buildImagePreview() {
if (_selectedImage != null) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
File(_selectedImage!.path),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.error, color: Colors.red);
},
),
);
} else if (_imageUrl != null) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
_imageUrl!,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red),
Text('Erreur chargement'),
],
);
},
),
);
} else {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo_camera, size: 50, color: Colors.grey),
SizedBox(height: 8),
Text('Aucune image',
style: TextStyle(color: Colors.grey),
),
],
);
}
}
}
storage_page.dart
// 📁 lib/pages/storage_page.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../services/storage_service.dart';
import '../models/stored_file.dart';
import '../widgets/image_upload_widget.dart';
class StoragePage extends StatefulWidget {
const StoragePage({Key? key}) : super(key: key);
@override
_StoragePageState createState() => _StoragePageState();
}
class _StoragePageState extends State {
final StorageService _storageService = StorageService();
final FirebaseAuth _auth = FirebaseAuth.instance;
List _userImages = [];
bool _isLoading = false;
int _storageUsage = 0;
@override
void initState() {
super.initState();
_loadUserImages();
}
// 📥 Charger les images de l'utilisateur
Future _loadUserImages() async {
setState(() => _isLoading = true);
try {
final user = _auth.currentUser;
if (user != null) {
_userImages = await _storageService.getUserImages(user.uid);
_storageUsage = await _storageService.getStorageUsage(user.uid);
}
} catch (e) {
_showSnackbar('Erreur chargement: $e');
} finally {
setState(() => _isLoading = false);
}
}
// 📤 Uploader une nouvelle image
Future _uploadImage(XFile imageFile) async {
try {
final user = _auth.currentUser;
if (user == null) throw Exception('Utilisateur non connecté');
setState(() => _isLoading = true);
final StoredFile uploadedFile = await _storageService.uploadImage(
imageFile: imageFile,
userId: user.uid,
);
setState(() {
_userImages.insert(0, uploadedFile);
_storageUsage += uploadedFile.size;
});
_showSnackbar('Image uploadée avec succès!');
} catch (e) {
_showSnackbar('Erreur upload: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
// 🗑️ Supprimer une image
Future _deleteImage(int index) async {
final StoredFile fileToDelete = _userImages[index];
final bool confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Confirmer la suppression'),
content: Text('Voulez-vous vraiment supprimer "${fileToDelete.name}"?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm) {
try {
await _storageService.deleteImage(fileToDelete.path);
setState(() {
_storageUsage -= fileToDelete.size;
_userImages.removeAt(index);
});
_showSnackbar('Image supprimée');
} catch (e) {
_showSnackbar('Erreur suppression: $e', isError: true);
}
}
}
void _showSnackbar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
),
);
}
// 📊 Formater la taille en octets
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Firebase Storage'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadUserImages,
tooltip: 'Rafraîchir',
),
],
),
body: _isLoading && _userImages.isEmpty
? Center(child: CircularProgressIndicator())
: Column(
children: [
// 📈 Statistiques de stockage
Card(
margin: EdgeInsets.all(16),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
Text('${_userImages.length}',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
Text('Images', style: TextStyle(color: Colors.grey)),
],
),
Column(
children: [
Text(_formatFileSize(_storageUsage),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
Text('Stockage utilisé', style: TextStyle(color: Colors.grey)),
],
),
],
),
),
),
// 📤 Widget d'upload
Padding(
padding: EdgeInsets.all(16),
child: ImageUploadWidget(
onImageSelected: _uploadImage,
),
),
// 📜 Liste des images
Expanded(
child: _userImages.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo_library, size: 80, color: Colors.grey),
SizedBox(height: 16),
Text('Aucune image uploadée',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
)
: GridView.builder(
padding: EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
),
itemCount: _userImages.length,
itemBuilder: (context, index) {
return _buildImageCard(_userImages[index], index);
},
),
),
],
),
);
}
Widget _buildImageCard(StoredFile file, int index) {
return Card(
elevation: 4,
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Image
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
child: Image.network(
file.url,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: Icon(Icons.error, color: Colors.red),
);
},
),
),
),
// Infos fichier
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name.length > 20
? '${file.name.substring(0, 20)}...'
: file.name,
style: TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Text(
_formatFileSize(file.size),
style: TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
'${file.uploadDate.day}/${file.uploadDate.month}/${file.uploadDate.year}',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
],
),
// Bouton suppression
Positioned(
top: 8,
right: 8,
child: CircleAvatar(
radius: 14,
backgroundColor: Colors.red.withOpacity(0.8),
child: IconButton(
icon: Icon(Icons.delete, size: 14, color: Colors.white),
onPressed: () => _deleteImage(index),
padding: EdgeInsets.zero,
),
),
),
],
),
);
}
}
main.dart
// 📁 lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'pages/storage_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Storage Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: StoragePage(),
debugShowCheckedModeBanner: false,
);
}
}
storage_page.dart
// 📁 Firebase Console > Storage > Rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Autoriser l'upload seulement aux utilisateurs authentifiés
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Images publiques (lecture seulement)
match /public/{allPaths=**} {
allow read: if true;
allow write: if request.auth != null;
}
// Par défaut, refuser tout accès
match /{allPaths=**} {
allow read, write: if false;
}
}
}
storage_page.dart
