![](assets/img/portfolio/cloudgallery-desktop.jpg)
![](assets/img/portfolio/cloudgallery-tablet.jpg)
![](assets/img/portfolio/cloudgallery-mobile.jpg)
Project links
- Github (and Documentation): https://github.com/MichaelRinglein/CloudGallery
- Live Demo: https://flutterwebapps.com/portfolio/cloud-gallery/#/
- Play Store: https://play.google.com/store/apps/details?id=com.strawanzer.cloudgallery
Cloud Gallery | Progressive Web App, Android, Firebase, Gallery
With this project I want to demonstrate and use Flutter's promise of one codebase for all platforms (Android, iOS, Web).
A user can log in, pick an image from the device's gallery or computer's drive and save it to a Firebase Storage (coming soon).
Where necessary I use the kIsWeb
boolean to display some widget particular for web or for native (the signInWithGoogleNative()
and signInWithGoogleWeb()
for example).
Functionalities
- Register / Sign In anonymously
- Register / Sign In with Google
- Picking images from desktop or mobile
- Uploading images to Firebase Cloud
- Retrieving already uploaded images from Firebase Cloud
- Delete uploaded images from Firebase Cloud
Packages used
- Firebase Auth
- Firebase Google Sign In
- Firebase Firestore
- Provider
- Image Picker
- Photo View
Documentation
main.dart
Firebase is initialized here on the top of the widget tree. A Futurebuilder is used, the future is the Firebase.initializeApp()
future. On successful connection to Firebase the next Widget in the tree is shown, the Wrapper()
wrapper.dart
This Widget is between the main MyApp() and all the following widgets and pages. This Wrapper() discriminates between the user logged in (is already registerd) or logged out (need to register).
A StreamBuilder is listening to the authStateChanges()
stream of Firebase. If the user is logged in, the Home() widget is shown. If he is logged out / hasn’t registered yet, the SignIn() widget is shown.
loading.dart
A Flutter SpinKitRing
is shown with a Text
widget below. The Loading()
class is a stateless widget with the loadingText
as parameter. The Loading
widget can be inserted everywhere in the app with a customnized loadingText
depending on the task where this widget is inserted.
home.dart
The boolean kIsWeb
is used to discriminate between web and native. If it is true, ImagePickerWeb() is shown, if it is false then ImagePickerNative() is shown.
auth/sign_in.dart
The screen to either sign in anonymously or with Google. kIsWeb
and Platform.isAndroid
are used in a if-else loop to iscriminate between web and native.
database/auth.dart
The Firebase authentication methods are stored here. I prefer Future classes with try-catch loops so the app doesn’t crash and the user gets an error (from the sign_in.dart) in case the authentication doesn’t work.
image_picker.dart
uploadFromGallery()
picks the image from the computer or device and saves it into the State imageFileWeb
or imageFileNative
. Since Web and Android/iOS handle images a bit different, the following distinction is made:
For Web the image’s path pickedFile.path
is set in imageFileWeb
and displayed in an Image.network
widget.
For Android the File(pickedFile.path)
is set in imageFileNative
and displayed in an Image.file
widget.
deletePickedImage()
is deleting the image by setting the State of imageFileWeb
and imageFileNative
back to null
.
uploadImageToFirebase()
takes the picked XFile
and the user
object as parameters.
To follow the progress of the upload to Firebase, a StreamBuilder
is used which takes the snapshots()
from the getProgress()
Stream in storage.dart.
show_images.dart
Here the images from the Firebase Storage are loaded and shown. To achieve this a FutureBuilder
is within the build function of a StreamProvider
.
The StreamProvider
is necessary to re-render the Widget when an image is uploaded (via the Stream of database/storage.dart
).
The FutureBuilder
is necessary since downloadURL()
from the database/storage.dart
returns a Future
.
So when the StreamProvider
updates and the build function is fired, the FutureBuilder
is build again as well (and the newly uploaded image as well).
Within the FutureBuilder
is a GridView.builder()
which includes every single image in a GestureDetector
. On Tap openImage()
is fired.
openImage()
opens the single image on tap in a Navigator.push()
. In this new MaterialPageRoute
the PhotoView()
package is used to display the single image.
database/storage.dart
Future uploadImageToFirebase()
uploadImageToFirebase()
receives the picked XFile
and the user
object as parameters from the image_picker.dart.
For Web .putFile()
is used. The image XFile
is transformed to a Uint8List
via the readAsBytes()
method. This solution a modified version of this post on StackOverflow.
The image is saved via the filePath
in a folder in the Firebase Storage which contains the user.id
(later in the development several images can be uploaded to the same user id).
Future getDownloadURLs()
First all saved image in the storages are retrieved via listAll()
and saved in the ListResult
class of Firebase. Next the Listresult
is looped through and of each element (each saved image) the download url is take via getDownloadURL()
and saved in downloadURLs
. downloadURLs
is a normal List
which is returned from the Future.
Stream getDownloadURLStream()
This is just generating a Stream
from the snapshots
method of Firebase. This is necessary to build the Streambuilder
in show_images.dart
. A change in the collection (when an image is uploaded) will be registered by this.
Future deleteImage()
This function takes in the downloadURL
and User
from the show_images.dart
.
Two things need to be deleted, the actual image in the Firebase Storage and the Reference to the image in Firebase Firestore. For the Storage a refFromURL
is taken since the downloadURL
is given. For the Firestore the .doc()
with the user id as parameter is taken.
Both, the Storage and the Firestore references are then deleted with the .delete()
function.
Configuration of the Firebase Storage
For web, a cors.json needed to be set for the Firebase Storage in the Google Cloud console. Those instructions from StackOverflow were followed. Also the gsutil cors set cors.json gs://...
was set with the bucket of this app.
Why Firebase Storage and Firestore
Firebase comes with an limitation regarding the storage: It is now possible to create a stream from the files in Storege.
Therefore this app saves a reference of all uploaded images of every user in a Firestore. With this reference a Stream is build. The Stream in turn is used in a StreamBuilder to update the FutureBuilder which recieves the downloadURLs for the images.
So when an images is saved to Storage, the reference is also saved in Firestore. This triggers the Stream and StreamBuilder and thus re-renders the FutureBuilder that retrieves the saved image (along with the previos saved images).