Overview
Firebase Storage provides secure file uploads and downloads with built-in security rules. This app uses Firebase Storage for profile image uploads.
Storage Bucket: aplicacion-trello.appspot.com
Implementation Locations:
AuthService: lib/services/auth_service.dart:14
Profile Editor: lib/ui/screens/edit_profile_screen.dart:20
Dependencies
Ensure these packages are in your pubspec.yaml:
dependencies :
firebase_storage : any
firebase_auth : any
image_picker : any # For selecting images
logger : any # For logging
Install dependencies:
Enabling Firebase Storage
Create Storage Bucket
Open Firebase Console
Select your project (aplicacion-trello)
Navigate to Storage in the left sidebar
Click Get started
Review security rules (start in test mode for development)
Choose a Cloud Storage location
Click Done
Configure Storage Bucket
Your storage bucket URL will be: gs://aplicacion-trello.appspot.com
This is automatically configured in firebase_options.dart: storageBucket : 'aplicacion-trello.appspot.com'
Storage Structure
The app uses the following directory structure:
aplicacion-trello.appspot.com/
├── profileImages/
│ ├── {userId}.jpg # User profile images (from registration)
│ └── ...
└── profile_images/
├── {userId} # User profile images (from profile editor)
└── ...
Note: The app uses two different paths for profile images. Consider standardizing to a single path for consistency.
File Naming Conventions
Profile Images from Registration
Used in AuthService.saveUserProfile() at line 35:
final storageRef = _storage. ref (). child ( 'profileImages/ $ uid .jpg' );
Path: profileImages/{userId}.jpg
Format: JPEG
Naming: User ID with .jpg extension
Use case: Initial profile image during user registration
Profile Images from Editor
Used in EditProfileScreen._updateProfile() at line 84:
final ref = _storage. ref (). child ( 'profile_images/ ${ user . uid } ' );
Path: profile_images/{userId}
Format: Original format (no extension)
Naming: User ID without extension
Use case: Profile image updates
Implementation Examples
Uploading Profile Image (Registration)
From lib/services/auth_service.dart:31:
lib/services/auth_service.dart
import 'dart:io' ;
import 'package:firebase_storage/firebase_storage.dart' ;
import 'package:cloud_firestore/cloud_firestore.dart' ;
import 'package:logger/logger.dart' ;
class AuthService {
final FirebaseStorage _storage = FirebaseStorage .instance;
final FirebaseFirestore _firestore = FirebaseFirestore .instance;
final Logger _logger = Logger ();
/// Save user profile with optional profile image
Future < void > saveUserProfile (
String uid,
String name,
String surname,
File ? profileImage
) async {
try {
String ? photoUrl;
// Upload profile image if provided
if (profileImage != null ) {
// Create storage reference with user ID
final storageRef = _storage. ref (). child ( 'profileImages/ $ uid .jpg' );
// Upload file to Firebase Storage
await storageRef. putFile (profileImage);
// Get download URL for the uploaded file
photoUrl = await storageRef. getDownloadURL ();
_logger. i ( 'Profile image uploaded successfully: $ photoUrl ' );
}
// Save user profile to Firestore
await _firestore. collection ( 'users' ). doc (uid). set ({
'name' : name,
'surname' : surname,
'photoUrl' : photoUrl,
'createdAt' : FieldValue . serverTimestamp (),
});
_logger. i ( 'User profile saved successfully' );
} catch (e) {
_logger. e ( 'Error saving user profile: $ e ' );
rethrow ;
}
}
}
Uploading Profile Image (Profile Editor)
From lib/ui/screens/edit_profile_screen.dart:68:
lib/ui/screens/edit_profile_screen.dart
import 'dart:io' ;
import 'package:flutter/material.dart' ;
import 'package:firebase_auth/firebase_auth.dart' ;
import 'package:firebase_storage/firebase_storage.dart' ;
import 'package:cloud_firestore/cloud_firestore.dart' ;
import 'package:image_picker/image_picker.dart' ;
import 'package:logger/logger.dart' ;
class EditProfileScreenState extends State < EditProfileScreen > {
final FirebaseAuth _auth = FirebaseAuth .instance;
final FirebaseStorage _storage = FirebaseStorage .instance;
final FirebaseFirestore _firestore = FirebaseFirestore .instance;
final Logger logger = Logger ();
File ? _profileImage;
String ? _photoUrl;
/// Pick image from gallery
Future < void > _pickImage () async {
final picker = ImagePicker ();
final pickedFile = await picker. pickImage (
source : ImageSource .gallery,
maxWidth : 1024 , // Limit image width
maxHeight : 1024 , // Limit image height
imageQuality : 85 , // Compress image (0-100)
);
if (pickedFile != null ) {
setState (() {
_profileImage = File (pickedFile.path);
});
}
}
/// Update user profile with new image
void _updateProfile () async {
User ? user = _auth.currentUser;
if (user == null ) return ;
try {
// Upload new profile image if selected
if (_profileImage != null ) {
// Create storage reference
final ref = _storage. ref (). child ( 'profile_images/ ${ user . uid } ' );
// Upload file with progress tracking
final uploadTask = ref. putFile (_profileImage ! );
// Optional: Listen to upload progress
uploadTask.snapshotEvents. listen (( TaskSnapshot snapshot) {
final progress = snapshot.bytesTransferred / snapshot.totalBytes;
logger. i ( 'Upload progress: ${( progress * 100 ). toStringAsFixed ( 2 )} %' );
});
// Wait for upload to complete
await uploadTask;
// Get download URL
_photoUrl = await ref. getDownloadURL ();
// Update Firestore with new photo URL
await _firestore. collection ( 'users' ). doc (user.uid). update ({
'photoUrl' : _photoUrl,
'updatedAt' : FieldValue . serverTimestamp (),
});
logger. i ( 'Profile image updated successfully' );
}
if (mounted) {
ScaffoldMessenger . of (context). showSnackBar (
const SnackBar (content : Text ( 'Profile updated successfully' )),
);
Navigator . pop (context);
}
} catch (e) {
logger. e ( 'Error updating profile: $ e ' );
if (mounted) {
ScaffoldMessenger . of (context). showSnackBar (
SnackBar (content : Text ( 'Error updating profile: $ e ' )),
);
}
}
}
}
Image Optimization
Optimize images before upload to reduce storage costs and improve performance:
Using ImagePicker Compression
final picker = ImagePicker ();
final pickedFile = await picker. pickImage (
source : ImageSource .gallery,
maxWidth : 1024 , // Resize to max 1024px width
maxHeight : 1024 , // Resize to max 1024px height
imageQuality : 85 , // Compress to 85% quality (0-100)
);
Advanced Compression with flutter_image_compress
Add to pubspec.yaml:
dependencies :
flutter_image_compress : any
Compress before upload:
import 'package:flutter_image_compress/flutter_image_compress.dart' ;
Future < File ?> compressImage ( File file) async {
final result = await FlutterImageCompress . compressAndGetFile (
file.absolute.path,
' ${ file . parent . path } /compressed_ ${ file . path . split ( '/' ). last } ' ,
quality : 85 ,
minWidth : 1024 ,
minHeight : 1024 ,
);
return result != null ? File (result.path) : null ;
}
Storage Security Rules
Configure security rules in Firebase Console:
Production Rules
rules_version = '2' ;
service firebase . storage {
match / b / { bucket } / o {
// Helper function to check if user is authenticated
function isAuthenticated () {
return request . auth != null ;
}
// Helper function to check if user owns the resource
function isOwner ( userId ) {
return request . auth . uid == userId ;
}
// Helper function to validate file size (5MB max)
function isValidSize () {
return request . resource . size < 5 * 1024 * 1024 ;
}
// Helper function to validate image type
function isImage () {
return request . resource . contentType . matches ( 'image/.*' );
}
// Profile images from registration (profileImages/{userId}.jpg)
match / profileImages / { userId }. jpg {
// Anyone authenticated can read
allow read : if isAuthenticated ();
// Only owner can write, with validation
allow write : if isOwner ( userId )
&& isImage ()
&& isValidSize ();
}
// Profile images from editor (profile_images/{userId})
match / profile_images / { userId } {
// Anyone authenticated can read
allow read : if isAuthenticated ();
// Only owner can write, with validation
allow write : if isOwner ( userId )
&& isImage ()
&& isValidSize ();
}
// Fallback: deny all other access
match / { allPaths =** } {
allow read , write : if false ;
}
}
}
Never use test mode rules in production: // NEVER USE THIS IN PRODUCTION!
allow read , write : if true ;
Always restrict access based on authentication and ownership.
Development Rules
For development only:
rules_version = '2' ;
service firebase . storage {
match / b / { bucket } / o {
match / { allPaths =** } {
// Allow authenticated users during development
allow read , write : if request . auth != null ;
}
}
}
Handling Upload Errors
Error Types and Solutions
Unauthorized Error
File Too Large
Network Error
Complete Error Handling
try {
await storageRef. putFile (file);
} on FirebaseException catch (e) {
if (e.code == 'unauthorized' ) {
// User doesn't have permission to upload
logger. e ( 'Upload unauthorized: Check security rules' );
showError ( 'You do not have permission to upload files' );
}
}
Displaying Profile Images
Using NetworkImage
CircleAvatar (
radius : 50 ,
backgroundImage : _photoUrl != null
? NetworkImage (_photoUrl ! )
: null ,
child : _photoUrl == null
? const Icon ( Icons .person, size : 50 )
: null ,
)
Using CachedNetworkImage
Add dependency:
dependencies :
cached_network_image : any
Implementation:
import 'package:cached_network_image/cached_network_image.dart' ;
CachedNetworkImage (
imageUrl : _photoUrl ?? '' ,
placeholder : (context, url) => const CircularProgressIndicator (),
errorWidget : (context, url, error) => const Icon ( Icons .error),
imageBuilder : (context, imageProvider) => CircleAvatar (
radius : 50 ,
backgroundImage : imageProvider,
),
)
Deleting Files
Delete Old Profile Image
Future < void > deleteOldProfileImage ( String userId) async {
try {
// Delete from both possible locations
final ref1 = _storage. ref (). child ( 'profileImages/ $ userId .jpg' );
final ref2 = _storage. ref (). child ( 'profile_images/ $ userId ' );
await ref1. delete (). catchError ((e) {
logger. w ( 'No file at profileImages/ $ userId .jpg' );
});
await ref2. delete (). catchError ((e) {
logger. w ( 'No file at profile_images/ $ userId ' );
});
logger. i ( 'Old profile images deleted' );
} catch (e) {
logger. e ( 'Error deleting old images: $ e ' );
}
}
Storage Costs and Limits
Firebase Storage Pricing (Free Tier)
Storage: 5 GB
Downloads: 1 GB/day
Uploads: 20,000/day
Cost Optimization Tips
Compress images before upload (target 100-500KB per image)
Set size limits in security rules (e.g., 5MB max)
Delete old files when users update profile images
Use CDN caching for frequently accessed images
Implement lazy loading for image galleries
Testing Storage Upload
Test Image Selection
// Test ImagePicker integration
final picker = ImagePicker ();
final image = await picker. pickImage (source : ImageSource .gallery);
print ( 'Image selected: ${ image ?. path } ' );
Test Upload to Storage
// Test file upload
final file = File ( 'path/to/test/image.jpg' );
final ref = FirebaseStorage .instance. ref (). child ( 'test/upload.jpg' );
await ref. putFile (file);
print ( 'Upload successful' );
Test Download URL
// Test getting download URL
final url = await ref. getDownloadURL ();
print ( 'Download URL: $ url ' );
Verify in Firebase Console
Open Firebase Console
Navigate to Storage
Check that files appear in correct directories
Click on a file to view details and download URL
Troubleshooting
Firebase Storage: Object does not exist
Cause: File path doesn’t match or file was deleted.Solution:
Verify the file path matches exactly
Check Firebase Console to confirm file exists
Ensure file was uploaded successfully
Upload fails with 'unauthorized' error
Cause: Security rules deny access.Solution:
Check security rules in Firebase Console
Verify user is authenticated (request.auth != null)
Ensure userId matches authenticated user
Check file size and type restrictions
Network error during upload
Cause: Poor network connectivity or timeout.Solution:
Implement retry logic
Show upload progress to user
Reduce image size/quality
Check network connectivity before upload
Image doesn't display after upload
Cause: URL not saved correctly or cache issue.Solution:
Verify download URL is saved to Firestore
Check download URL in Firebase Console
Clear image cache
Add error handling to image widget
File size too large error
Cause: Image exceeds size limits.Solution:
Compress images before upload
Use ImagePicker quality parameters
Validate file size before upload
Show clear error message to user
Best Practices
Standardize file paths: Use consistent naming conventions
Validate files: Check size and type before upload
Compress images: Reduce file size for faster uploads
Handle errors gracefully: Provide clear error messages
Show upload progress: Keep users informed during long uploads
Implement retry logic: Handle network failures automatically
Clean up old files: Delete replaced profile images
Use security rules: Always validate permissions server-side
Cache images: Use CachedNetworkImage for better performance
Monitor usage: Track storage usage in Firebase Console
Next Steps