image

14 Dec 2025

9K

35K

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.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is