Flutter Firebase Storage: Upload Images & Files
Published: 2026-10-10 · 7 min read
Flutter Firebase Storage guide: pick and compress files, track upload progress, generate download URLs, wire into Firestore, and secure it all with rules.
Almost every Flutter app needs to handle files eventually: profile photos, chat attachments, scanned documents, user-generated content. Cloud Storage for Firebase (often called Firebase Storage) is the answer, and the Flutter Firebase Storage upload flow is simpler than most developers expect — pick a file, get an upload task, track progress, store the download URL.
This guide walks through the complete implementation: setup, uploading images and files with progress bars, generating download URLs, wiring uploads into Firestore, and the security rules and optimizations that keep the experience fast and safe.
What Firebase Storage Actually Is
Firebase Storage is Google Cloud Storage with Firebase's authentication and security rules bolted on. Files live in a bucket organized by path (users/{uid}/avatar.jpg), and every upload, download, and delete is governed by rules you define. Your Flutter app talks to it through the firebase_storage package — no server code required.
One architectural point that saves pain later: Storage holds the bytes, Firestore holds the metadata. Never query Storage to "list a user's photos" by crawling paths. Store each upload's download URL and metadata (size, content type, uploaded at) in a Firestore document, and query that.
Setup
dependencies:
firebase_storage: ^12.0.0
image_picker: ^1.0.0
Android: no extra manifest work for basic uploads. iOS: add photo library usage descriptions to Info.plist if you use image_picker:
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos so you can upload a profile picture.</string>
The Core Flutter Firebase Storage Upload Flow
Pick an image, convert to a File, and push it. The whole flow in one function:
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
Future<String> uploadProfilePhoto(String uid) async {
// 1. Pick the image
final picked = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxWidth: 1024, // resize on the client — huge win, see below
maxHeight: 1024,
imageQuality: 85,
);
if (picked == null) throw Exception('No image selected');
// 2. Reference the destination path
final ref = FirebaseStorage.instance.ref().child('users/$uid/avatar.jpg');
// 3. Upload and await completion
final uploadTask = ref.putFile(File(picked.path));
final snapshot = await uploadTask;
// 4. Get the public download URL
return await snapshot.ref.getDownloadURL();
}
That's the complete happy path. Everything below makes it production-grade.
Uploading Any File Type (Not Just Images)
putFile works for any file. For documents, videos, or audio, use a file picker and set the content type explicitly so downloads render correctly:
final ref = FirebaseStorage.instance
.ref()
.child('attachments/${DateTime.now().millisecondsSinceEpoch}.pdf');
await ref.putFile(
File(filePath),
SettableMetadata(contentType: 'application/pdf'),
);
SettableMetadata also accepts customMetadata — useful for storing the uploader's UID or original filename alongside the file.
Progress Tracking: The Upload Bar Users Expect
An upload without progress feedback feels broken. UploadTask exposes a snapshotEvents stream — bind it to your UI:
final task = ref.putFile(file);
// In a StatefulWidget or a Cubit:
task.snapshotEvents.listen((TaskSnapshot snapshot) {
final progress = snapshot.bytesTransferred / snapshot.totalBytes;
setState(() => _progress = progress); // or emit to your state class
});
await task;
Render _progress with a LinearProgressIndicator. Handle the three terminal states explicitly: TaskState.success, TaskState.error, and TaskState.canceled. A common pattern is to keep a map of uploadId -> UploadTask so users can cancel in-flight uploads:
void cancelUpload(UploadTask task) => task.cancel();
Storing the Result in Firestore
The download URL is useless if the app can't find it later. After a successful upload, write a Firestore document:
final downloadUrl = await uploadProfilePhoto(uid);
await FirebaseFirestore.instance.collection('users').doc(uid).set({
'photoUrl': downloadUrl,
'photoPath': 'users/$uid/avatar.jpg', // keep the path! Needed to delete later
'photoUpdatedAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
Storing photoPath matters: getDownloadURL() gives you a URL, but only the storage path lets you delete or replace the file. I see this forgotten in code reviews constantly.
Deleting and Replacing Files
// Delete a file you previously uploaded
await FirebaseStorage.instance.ref().child('users/$uid/avatar.jpg').delete();
// Replace: upload overwrites the same path automatically
await FirebaseStorage.instance.ref().child('users/$uid/avatar.jpg')
.putFile(newFile);
Note: overwriting the same path keeps the same download URL (the URL is per-object, and an overwrite creates a new generation that the URL still resolves to — with caching caveats). For avatars and frequently-replaced images, use a versioned path (avatar_v2.jpg) or append a timestamp to bust caches reliably.
Optimizing Uploads: Shrink Before You Send
The single biggest performance win in any Flutter Firebase Storage upload implementation is client-side compression. Users upload 12 MB photos over mobile data; resizing to 1024 px and 85% quality drops that to ~200 KB with no visible difference on a phone screen:
final picked = await ImagePicker().pickImage(
source: ImageSource.camera,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
);
For thumbnails, generate a second small copy and upload both. Two cheap uploads beat one giant download on every list scroll.
Security Rules for Storage
Never ship allow read, write: if true. A sane starting point — users can only write their own folder:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5 MB cap
&& request.resource.contentType.matches('image/.*'); // images only
}
}
}
Rules can validate size and content type server-side, so even a malicious client can't bypass your limits.
Flutter Firebase Storage Upload Pitfalls
- Forgetting
awaiton the task and immediately callinggetDownloadURL()— the URL doesn't exist until the upload completes. Alwaysawait taskfirst. - Uploading from web without
putBlob/putData:dart:ioFile doesn't exist on web. UseXFile.readAsBytes()andputData(bytes)for web-compatible code. - No retry on flaky networks: wrap uploads in a retry helper with exponential backoff; mobile networks drop mid-upload routinely.
- Storing download URLs in Storage paths: URLs contain tokens and change; keep the storage path in Firestore and derive URLs at read time or cache them.
- Ignoring
TaskState.paused: on Android, uploads pause when the app backgrounds. Resume withtask.resume()or handle it in your state layer.
Putting It All Together
A solid upload implementation is five pieces: pick and compress the file, stream progress to the UI, await completion before fetching the URL, record the path plus URL in Firestore, and guard everything with rules. Nail that pattern once and you can reuse it for avatars, chat media, documents, and anything else your app needs.
Building a Flutter app with media uploads, chat, or user-generated content? Get in touch — I've shipped this exact pipeline in production apps and can get yours right the first time.
Frequently Asked Questions
How do I upload a file to Firebase Storage from Flutter?
Pick the file, upload with ref.putFile(file), await the UploadTask, then call snapshot.ref.getDownloadURL(). The URL does not exist until the upload completes, so always await the task first.
Should I store the download URL or the storage path in Firestore?
Store the storage path (e.g. users/uid/avatar.jpg) alongside the download URL in Firestore. Only the path lets you delete or replace the file later — the URL is per-object and cannot be used for deletion.
How do I show upload progress for Firebase Storage uploads?
Use UploadTask.snapshotEvents, which streams TaskSnapshot updates; compute progress as bytesTransferred divided by totalBytes and render it with a LinearProgressIndicator. Handle TaskState.success, error, and canceled explicitly, and support cancellation with task.cancel().
How can I reduce Firebase Storage upload sizes in Flutter?
Resizing to 1024 px with 85% quality can drop a 12 MB phone photo to around 200 KB with no visible difference on a phone screen. For lists, generate a second small thumbnail copy so scrolling does not trigger giant downloads.