Flutter & Firebase Storage: Image Upload with Progress Indicator
Building modern mobile applications often involves handling file uploads, and images are a common use case. When users upload files, especially larger ones, providing visual feedback through a progress indicator significantly enhances the user experience. This article will guide you through implementing image uploads to Firebase Storage using Flutter, complete with real-time progress tracking.
Why Flutter & Firebase Storage?
Flutter, with its single codebase for multiple platforms, and Firebase Storage, a robust, scalable object storage service built by Google, form a powerful combination for developing feature-rich applications quickly. Firebase Storage handles the complexities of file storage, security rules, and scaling, allowing developers to focus on the application logic.
Prerequisites
- A Flutter development environment set up.
- A Firebase project created and connected to your Flutter app. If you haven't done this, refer to the official Firebase Flutter setup guide.
- Basic understanding of Flutter widgets and state management.
Step 1: Firebase Storage Setup
1.1 Enable Firebase Storage
Navigate to your Firebase project console, go to "Build" > "Storage", and click "Get Started". Choose a security rules set (for development, "test mode" is fine initially, but remember to secure it for production).
1.2 Configure Storage Rules (Optional but Recommended)
For simple image uploads, you might allow authenticated users to write. Here's a basic example:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
Step 2: Add Dependencies to Flutter Project
Open your pubspec.yaml file and add the following dependencies:
dependencies:
flutter:
sdk: flutter
image_picker: ^1.0.4 # Or the latest version
firebase_core: ^2.24.2 # Or the latest version
firebase_storage: ^11.5.6 # Or the latest version
Run flutter pub get to fetch the new packages.
Step 3: Pick an Image from Gallery/Camera
We'll use the image_picker package to allow users to select an image.
import 'dart:io';
import 'package:image_picker/image_picker.dart';
Future<File?> pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
return File(pickedFile.path);
}
return null;
}
Step 4: Upload Image to Firebase Storage with Progress
This is the core logic. We'll use firebase_storage to upload the file and listen to snapshotEvents to track progress.
import 'package:firebase_storage/firebase_storage.dart';
import 'dart:io';
class ImageUploader {
final FirebaseStorage _storage = FirebaseStorage.instance;
UploadTask? uploadFile(File file, String path, Function(double) onProgress) {
try {
final Reference ref = _storage.ref().child(path);
final UploadTask uploadTask = ref.putFile(file);
uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
final double progress = snapshot.bytesTransferred / snapshot.totalBytes;
onProgress(progress);
});
return uploadTask;
} catch (e) {
print('Error uploading file: $e');
return null;
}
}
}
Step 5: Integrate into a Flutter Widget
Let's create a simple StatefulWidget to manage the image selection, upload, and progress display.
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:firebase_core/firebase_core.dart'; // Required for Firebase.initializeApp()
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
// Ensure Firebase is initialized before runApp()
// void main() async {
// WidgetsFlutterBinding.ensureInitialized();
// await Firebase.initializeApp();
// runApp(MyApp());
// }
// class MyApp extends StatelessWidget {
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// title: 'Flutter Firebase Storage Upload',
// theme: ThemeData(
// primarySwatch: Colors.blue,
// ),
// home: ImageUploadScreen(),
// );
// }
// }
class ImageUploadScreen extends StatefulWidget {
const ImageUploadScreen({super.key});
@override
State<ImageUploadScreen> createState() => _ImageUploadScreenState();
}
class _ImageUploadScreenState extends State<ImageUploadScreen> {
File? _imageFile;
double _uploadProgress = 0.0;
String _uploadStatus = 'No file selected';
UploadTask? _uploadTask; // To manage ongoing upload
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_imageFile = File(pickedFile.path);
_uploadProgress = 0.0; // Reset progress for new file
_uploadStatus = 'Image selected';
});
}
}
Future<void> _uploadImage() async {
if (_imageFile == null) {
setState(() {
_uploadStatus = 'Please select an image first.';
});
return;
}
final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
final String destination = 'uploads/$fileName'; // Path in Firebase Storage
setState(() {
_uploadStatus = 'Uploading...';
_uploadProgress = 0.0;
});
try {
final Reference ref = FirebaseStorage.instance.ref().child(destination);
_uploadTask = ref.putFile(_imageFile!);
_uploadTask!.snapshotEvents.listen((TaskSnapshot snapshot) {
setState(() {
_uploadProgress = snapshot.bytesTransferred / snapshot.totalBytes;
_uploadStatus = 'Uploading: ${(_uploadProgress * 100).toStringAsFixed(0)}%';
});
}, onError: (e) {
setState(() {
_uploadStatus = 'Upload failed: $e';
_uploadProgress = 0.0;
});
print('Upload error: $e');
});
// Wait for the upload to complete
await _uploadTask!;
setState(() {
_uploadStatus = 'Upload complete!';
_uploadProgress = 1.0; // Ensure it shows 100%
});
print('Upload complete! Download URL: ${await ref.getDownloadURL()}');
} on FirebaseException catch (e) {
setState(() {
_uploadStatus = 'Upload failed: ${e.message}';
_uploadProgress = 0.0;
});
print('Firebase upload error: $e');
} catch (e) {
setState(() {
_uploadStatus = 'Upload failed: $e';
_uploadProgress = 0.0;
});
print('General upload error: $e');
} finally {
_uploadTask = null; // Clear task after completion or error
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Image Upload to Firebase Storage'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
ElevatedButton(
onPressed: _pickImage,
child: const Text('Select Image'),
),
const SizedBox(height: 20),
_imageFile != null
? Image.file(
_imageFile!,
height: 150,
width: 150,
fit: BoxFit.cover,
)
: const Text('No image selected'),
const SizedBox(height: 20),
if (_uploadTask != null)
LinearProgressIndicator(
value: _uploadProgress,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
),
const SizedBox(height: 10),
Text(_uploadStatus),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _uploadTask == null && _imageFile != null ? _uploadImage : null,
child: const Text('Upload Image'),
),
],
),
),
);
}
}
Remember to initialize Firebase in your main function:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // Initialize Firebase
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Storage Upload',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ImageUploadScreen(),
);
}
}
Conclusion
Implementing image uploads with progress indicators in Flutter using Firebase Storage is a straightforward process that significantly improves user experience. By following these steps, you can provide clear, real-time feedback to your users, making your application feel more responsive and professional. Remember to always consider security rules and error handling in a production environment.