Pick and upload images to Firebase with Flutter

·

7 min read

Table of contents

No heading

No headings in the article.


fahim-muntashir-v-FOvoL3onk-unsplash.jpg Photo by Fahim Muntashir on Unsplash.

This post shares my challenges with picking multiple images, uploading to Firebase storage, and managing state using the Bloc pattern.

Table of contents

  • What is Flutter?
  • Audience
  • What the post is about
  • Goals for the post
  • Setup

What is Flutter?

Flutter is a cross-platform that supports the development of apps for iOS, Android, web, and desktop from a single code-base. Read more here.

What is Firebase?

I could not put it better than this.

Audience

Intermediate. You should be familiar with the Bloc pattern. Read more about the Bloc pattern here if you are just getting started.

What we are building

A flutter app that enables a user to pick photos from their phone and upload them to Firebase Cloud Storage. This was a challenge for me when I was building a similar app. I didn't know how to retrieve the list of links from cloud storage and how to use those links to create documents in the Firebase collection. I struggled for a few days before solving the problem. This post is meant to make your life better while working with images in flutter.

All code for this tutorial will be hosted on GitHub and links will be added at appropriate sections of the blog.


Goals for the post

By the end of the post, you should be able to;

  • Develop an app that allows users to pick multiple images and upload them to cloud storage.
  • Use the Bloc pattern to update the state of your app when images are picked by the user.
  • Use Firebase cloud storage to store data such as images on Firebase, and Firestore collections to store metadata about an image. You should also be comfortable with Firestore rules for protecting user's data in your app.

Getting started

To get started, create a new flutter app by running:

$ flutter create [app_name] && cd [app_name] && code .

With the above commands;

1- We created a new app. 2- Changed the directory to our app's folder, and opened the folder in VSCode .

You can also open the folder using Android Studio if VSCode is not installed on your machine by right-clicking on the folder [app_name] and choosing Open with other application (on Ubuntu) option then choosing Android Studio.

Packages used

Add these packages to pubspec.yaml file.

flutter_bloc: ^7.1.0
firebase_core: ^1.4.0
cloud_firestore: ^2.2.0
firebase_storage: ^8.1.0
permission_handler: ^7.1.0
multi_image_picker2: ^5.0.0
path_provider: ^2.0.2

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Photo sharing app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
    );
  }
}

lib/screens/home_screen.dart

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: Text('Photo Sharing app'),),
  );
}

1_0LiRjgHn_nElxStfysyIsw.png

Error: [[!] The shrinker may have failed to optimize the Java byte-code. To disable the shrinker, pass the - no-shrink flag to this command. To learn more, see: developer.android.com/studio/build/shrink-c..

build.gradle

defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.photo_sharing_app"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
}

Adding app to Firebase

Follow these instructions.

Let's upload an image to storage (manually)

Click on Upload file button and pick a local image file to upload.

Add uploaded image to images collection in Firebase Firestore

Now let's grab our image url so we can use to create a document. From Storage, click on the image and right click on the Name property to copy the link address. We will use the link address for the url field in the document. Now in the images collection, create a document with the url Add image document to collection


Let's create an image model

The new file should be created inside lib/models

import 'package:cloud_firestore/cloud_firestore.dart';

class ImageModel {
  const ImageModel({required this.url, required this.uploaded});

  final String url;

  final Timestamp uploaded;

  factory ImageModel.fromSnapshot(DocumentSnapshot doc) =>
      ImageModel(url: doc['url'], uploaded: doc['uploaded']);

  Map<String, dynamic> toDocument() => {'url': url, 'uploaded': uploaded};
}

Here are some reasons to use models in your app

  • A lightweight application 'model' can add value to an Agile development process.
  • User flows are the basis of an easily comprehended application model, which can be used to socialize and validate features and functions of the app with the wider team and stakeholders.
  • An application model can be used to effectively involve the UX function and create solutions prior to a coding iteration.
  • Application modeling assists with exploring the total project scope and facilitates selection of the user flows needed for a minimum desirable product.
  • An agile model allows and supports an iterative time-bounded project approach. It is easily updated, with as-built decisions, to ensure its usefulness in an ongoing product lifecycle.

Add image repository

We will be using the repository pattern along side the bloc pattern. The repository is a data layer in the app's architecture. The repository is the link between the server/database and the app.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:photo_sharing_app/models/image_model.dart';

class ImageRepository {
  final _firebaseFirestore = FirebaseFirestore.instance;

  Stream<List<ImageModel>> images() =>
      _firebaseFirestore.collection("images").snapshots().map((snapshot) =>
          snapshot.docs.map((doc) => ImageModel.fromSnapshot(doc)).toList()
            ..sort((a, b) => b.uploaded.compareTo(a.uploaded)));
}

Let's move on the bloc

Let's code our business logic. The bloc will form our business logic. This is where we will alter the state of our app and send updates to the UI.

import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:photo_sharing_app/blocs/image_state.dart';
import 'package:photo_sharing_app/models/image_model.dart';
import 'package:photo_sharing_app/repositories/image_repository.dart';

class ImageCubit extends Cubit<ImageState> {
  ImageCubit({required ImageRepository imageRepository})
      : _imageRepository = imageRepository,
        super(ImageState());

  StreamSubscription<List<ImageModel>>? _imageStreamSubscription;

  final ImageRepository _imageRepository;

  void loadImages() {
    _imageStreamSubscription?.cancel();

    _imageStreamSubscription = _imageRepository
        .images()
        .listen((images) => emit(ImagesLoaded(images: images)));
  }

  void addImagesToState() async {
    try {
      final _files = await imagesToPreview();

      emit(state.copyWith(images: _files));
    } on Exception catch (_) {}
  }
}

We are using the Cubit here since the Bloc would be an over kill for this implementation. The Cubit allows for state update using functions unlike the Bloc where events are used to trigger changes to the app's state. A Bloc is a more advanced class which relies on events to trigger state changes rather than functions. Bloc also extends BlocBase which means it has a similar public API as Cubit. However, rather than calling a function on a Bloc and directly emitting a new state, Blocs receive events and convert the incoming events into outgoing states. Add a state to be updated by methods in image cubit


import 'package:photo_sharing_app/models/image_model.dart';

class ImageState {}

class ImagesLoaded extends ImageState {
  ImagesLoaded({required this.images});

  final List<ImageModel> images;
}

Now our updated main.dart file becomes

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:photo_sharing_app/blocs/image_cubit.dart';
import 'package:photo_sharing_app/repositories/image_repository.dart';
import 'package:photo_sharing_app/screens/home_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  runApp(BlocProvider<ImageCubit>(
    create: (context) =>
        ImageCubit(imageRepository: ImageRepository())..loadImages(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Photo sharing app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
    );
  }
}

Updated main.dart on line 9, we are initializing the flutter widgets before initializing our Firebase app. We always have to add WidgetsFlutterBinding.ensureInitialized() whenever we have code that needs to run before runApp() .

Line 13, provides a Bloc via the BlocProvider class to widgets below the MyApp widget in this case the HomeScreen. We will get an error saying there was no Bloc provided if we miss to wrap MyApp with BlocProvider.

We are also invoking the loadImages() method. This way, our widget gets populated with current images the minute we start the app.

Let's run our app…

1_yPR5BdMbJhYj4D-qbtpvQg.png


Note: In order to avoid the error: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions, let's update the rules in our Storage bucket and firestore.

1_jHUwAnVZa9-CuSNmnZ8jKw.jpeg


1_HB-VOHOmpId8-JO81woRqg.jpeg


Let's pick and upload images from our app

We will use the multi image picker package from pub.dev to pick our images. This package allows for multiple images to be picked and uploaded. This is exactly what we want.

It also allows for customization of how many images we can pick at once. I have limited the maximum number to 6 for this implementation.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:photo_sharing_app/blocs/image_cubit.dart';
import 'package:photo_sharing_app/blocs/image_state.dart';
import 'package:photo_sharing_app/widgets/preview_images.dart';

class ImagePreviewScreen extends StatelessWidget {
  const ImagePreviewScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(
        elevation: 0,
        title: Text(
          'Preview images to upload',
          style: TextStyle(
              color: Colors.white, letterSpacing: 1),
        ),
        backgroundColor: Theme.of(context).accentColor,
        iconTheme: Theme.of(context).iconTheme,
      ),
      body: BlocBuilder<ImageCubit, ImageState>(
          builder: (context, state) => Column(
                children: [
                  Expanded(
                      child: PreviewImages(assets: state.images ?? [])),
                  Padding(
                    padding: const EdgeInsets.only(bottom: 16),
                    child: IconButton(
                        icon: const Icon(
                          Icons.arrow_forward_ios_sharp,
                          size: 30,
                        ),
                        onPressed: () {}),
                  )
                ],
              )));
}

1_v1K_HlZQ-p4n1yxVRAbCXg.png


1_YtKd42cUNIDueM_U9WVDyQ.png


1_ngfKdA51wZVl4WXfA4jdpg.png


Finally, tap on the forward pointing arrow towards the bottom of the screen to upload the images to Firebase Storage.


Conclusion

This post was all about building a Flutter app for uploading images to Firebase Storage. At this point, you should be comfortable with;

  • Adding a Flutter Android app to Firebase.
  • Securing the database with Firestore rules.
  • Using the Bloc pattern to update state of the UI.
  • Uploading and saving images to Firebase Storage.
  • Fetching and rendering the images in Flutter app.

References

Articles and resources that made it possible for me to write this blog.