(Flutter Web) Firebase Storage not working with firebase plugin - firebase

I've recently added Flutter Web support for one of my projects which heavily uses Firebase services. Everything seems to work fine except Firebase Storage which doesn't work. I know that firebase_storage plugin currently doesn't work with Web so I tried instead with the regular firebase plugin but I can't get it to work.
I sometimes get different "red screen"-errors, but everything is related to pages which uses Firebase Storage. Here is one example of a red screen:
Here are the 3 files in which I previously used firebase_storage with success (Android) and with the code that I tried to get to work with th firebase plugin.
import 'package:firebase/firebase.dart' as fb;
// artiklar/images
final fb.StorageReference fbRefArtiklarImages =
fb.app().storage().ref().child("artiklar").child("images");
// guider/categoryImages
final fb.StorageReference fbRefGuiderCategoryImages =
fb.app().storage().ref().child("guider").child("categoryImages");
// guider/guideImages
final fb.StorageReference fbRefGuiderGuideImages =
fb.app().storage().ref().child("guider").child("guideImages");
// kalender/images
final fb.StorageReference fbRefKalenderImages =
fb.app().storage().ref().child("kalender").child("images");
// sidor/sidloggor
final fb.StorageReference fbRefSidorSidloggorImages =
fb.app().storage().ref().child("sidor").child("sidloggor");
// sidor/sidcovers
final fb.StorageReference fbRefSidorSidcoversImages =
fb.app().storage().ref().child("sidor").child("sidcovers");
// sidor/postImages/:sidaID/
final fb.StorageReference fbRefSidorPostImagesImages =
fb.app().storage().ref().child("sidor").child("postImages");
// sidor/postImages/:sidaID/
final fb.StorageReference fbRefSidorKalenderImagesImages =
fb.app().storage().ref().child("sidor").child("kalenderImages");
-
import 'dart:io';
import 'package:firebase/firebase.dart' as fb;
class StorageService {
//STORAGE REFERENCES
final fb.Storage _storage = fb.app().storage("gs://astoria-site.appspot.com");
//UPLOADS IMAGE TO FIREBASE
fb.UploadTask _uploadTask;
Future<void> uploadStorageImage(File imageFile, String filePath) async {
_uploadTask = _storage.ref().child(filePath).put(imageFile);
return;
}
//DELETES IMAGE IN FIREBASE
Future<void> deleteStorageImage(String filePath) async {
try {
await _storage.ref().child(filePath).delete();
} catch (e) {
print(e.toString());
}
return;
}
}
-
import 'package:astoria/theme/colors.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:firebase/firebase.dart';
import 'package:flutter/material.dart';
class FirebaseStorageImage extends StatelessWidget {
final String fileName;
final StorageReference storageLocation;
FirebaseStorageImage({
#required this.fileName,
#required this.storageLocation,
});
Future<String> _getImageURL() async {
final StorageReference ref = storageLocation.child(fileName + ".jpg");
try {
var url = await ref.getDownloadURL();
return url.toString();
} catch (e) {
return null;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getImageURL(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
return Image(
image: CachedNetworkImageProvider(snapshot.data),
fit: BoxFit.cover,
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
//RETURN THIS WHILE WAITING FOR IMAGE
return Container(color: lightGreyColor);
} else {
//RETURN THIS IF NO IMAGE WAS FOUND AT THAT LOCATION
return Image(
image: AssetImage("assets/images/placeholder.png"),
fit: BoxFit.cover,
);
}
},
);
}
}

You need to pass a string to the ref method for it to work.
For ease, change ref to refFromURL, then pass your bucket URL as a string to it as follows;
fb.app().storage().refFromURL("YOUR BUCKET URL HERE eg: 'gs://project-ID.appspot.com'")
Then you can safely add the remaining methods to it, such as; child() then put() or putString().
Goodluck!!!

For those coming after me, here's a summary of achieving Firebase Storage image download for Flutter Web.
Thanks to Learn Flutter Code for this nice little tutorial.
Don't make Firebase Storage a dependency, just Firebase with:
import 'package:firebase/firebase.dart' as fb;
Then create a method:
Future<Uri> myDownloadURL() async {return await fb.storage().refFromURL('gs://<your storage reference>').child('$id.jpg').getDownloadURL();}
Call it from a FutureBuilder like so:
FutureBuilder<Uri>(
future: myDownloadURL(),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return <Something as a placeholder>;
}
return CircleAvatar(
radius: backgroundRadius * 2,
child: Image.network(snapshot.data.toString()),
);
},
)

Related

Error in FirebaseAuth while using Provider

I want to create an entering page with firebase_auth. I can sign in successfully but I can't log in. When I try to log in application doesn't give any error but it isn't pass the main page. Still it is in log in page. When I restart the debugging now it pass the main page. Can you see my code and help me?
Here is my auth service code
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:library_app/models/person.dart';
class FirebaseAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Person? _createPerson(User? user) {
return user == null ? null : Person.fromFirebaseUser(user);
}
Stream<Person?> get statusFollower {
return _auth.authStateChanges().map(_createPerson);
}
void createUserEmailAndPassword({
required String email,
required String password,
}) async {
try {
var _userCredential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
_createPerson(_userCredential.user);
} catch (e) {
debugPrint(e.toString());
}
}
loginUserEmailAndPassword({
required String email,
required String password,
}) async {
try {
var _userCredential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
_createPerson(_userCredential.user);
} catch (e) {
debugPrint(e.toString());
}
}
void signOut() async {
await _auth.signOut();
}
}
And here is my orientation code
import 'package:flutter/cupertino.dart';
import 'package:library_app/models/person.dart';
import 'package:library_app/pages/error_page.dart';
import 'package:library_app/pages/loading_page.dart';
import 'package:library_app/pages/main_page.dart';
import 'package:library_app/pages/sign_in_page.dart';
import 'package:library_app/services/firebase_auth_service.dart';
import 'package:provider/provider.dart';
class OrientationSystem extends StatelessWidget {
const OrientationSystem({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var _authService = Provider.of<FirebaseAuthService>(context, listen: false);
return StreamBuilder<Person?>(
stream: _authService.statusFollower,
builder: (context, stream) {
if (stream.connectionState == ConnectionState.waiting) {
return const LoadingPage();
}
if (stream.hasData) {
return const MainPage();
}
if (!stream.hasData) {
return const SigInPage();
} else {
return const ErrorPage();
}
},
);
}
}
what must I do?
please help...
You did not extend or used with ChangeNotifier class in the FirebaseAuthService construction. Also, you need to notifyListener() after doing the tasks or after updating data inside your FirebaseAuthService class so that your other classes may listen to the updated values.
However, please update your question to something like "Error in FirebaseAuth while using Provider". Make your question titles as much relevant as you can otherwise you might get ban by the system. This is a suggestion.

Firebase _upload writes random downloadURL

Below is a simple firebase image uploader. The problem is that it sometimes uses another image's downloadURL as the value when it writes to Firestore. It uploads my image to cloud storage without a problem but then when it goes to write the location to firestore, it often uses the URL of another image. The full code is below but I have omitted the UI. How do I ensure that it writes the correct URL to firestore?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:path/path.dart' as path;
import 'package:image_picker/image_picker.dart';
class ImagePicky2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// Remove the debug banner
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.green),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
FirebaseStorage storage = FirebaseStorage.instance;
double? lat, lng;
File? file;
String? name, detail, pathImage, dateString;
// Select an image from the gallery or take a picture with the camera
// Then upload to Firebase Storage
Future<XFile?> _upload(String inputSource) async {
FirebaseAuth auth = FirebaseAuth.instance;
User firebaseUser = auth.currentUser!;
final picker = ImagePicker();
try {
final pickedImage = await picker.pickImage(
source: inputSource == 'camera'
? ImageSource.camera
: ImageSource.gallery,
imageQuality: 25,
maxWidth: 1920);
final String fileName = path.basename(pickedImage!.path);
File imageFile = File(pickedImage.path);
try {
// Uploading the selected image with some custom meta data
await storage.ref(fileName).putFile(
imageFile,
SettableMetadata(customMetadata: {
'uploaded_by': firebaseUser.displayName!,
'description': 'Some description...'
}));
// Refresh the UI
setState(() {});
} on FirebaseException catch (error) {
print(error);
}
} catch (err) {
print(err);
}
photoUploadFirestoreDetails();
}
// Retriew the uploaded images
// This function is called when the app launches for the first time or when an image is uploaded or deleted
Future<List<Map<String, dynamic>>> _loadImages() async {
FirebaseAuth auth = FirebaseAuth.instance;
User firebaseUser = auth.currentUser!;
List<Map<String, dynamic>> files = [];
final ListResult result = await storage.ref().list();
final List<Reference> allFiles = result.items;
await Future.forEach<Reference>(allFiles, (file) async {
final String fileUrl = await file.getDownloadURL();
pathImage = await file.getDownloadURL();
final FullMetadata fileMeta = await file.getMetadata();
files.add({
"url": fileUrl,
"path": file.fullPath,
"uploaded_by": fileMeta.customMetadata?['uploaded_by'] ?? firebaseUser.displayName,
"description":
fileMeta.customMetadata?['description'] ?? 'No description'
});
});
return files;
}
Future<Null> photoUploadFirestoreDetails() async {
Firebase.initializeApp();
Map<String, dynamic> map = Map();
map['PathImage'] = pathImage;
FirebaseFirestore firestore = FirebaseFirestore.instance;
CollectionReference collectionReference =
firestore.collection('MarkerCollect');
await collectionReference.doc().set(map).then((
value) {
});
}
}```
The code is uploading random download urls to Firestore because you're getting the image path from the _loadImages method which loads up the files on storage instead of using the download url of the just uploaded file.
This is the problematic code:
Future<Null> photoUploadFirestoreDetails() async {
...
map['PathImage'] = pathImage;
...
}
Solution:
You can fix this by retrieving the download url just after the upload and passing it to the photoUploadFirestoreDetails method to be used in the Firestore upload.
You should also put the photoUploadFirestoreDetails in the try-catch.
Checkout the updated code below:
// _upload method
Future<XFile?> _upload(String inputSource) async {
FirebaseAuth auth = FirebaseAuth.instance;
User firebaseUser = auth.currentUser!;
final picker = ImagePicker();
try {
final pickedImage = await picker.pickImage(
source: inputSource == 'camera'
? ImageSource.camera
: ImageSource.gallery,
imageQuality: 25,
maxWidth: 1920);
final String fileName = path.basename(pickedImage!.path);
File imageFile = File(pickedImage.path);
try {
// Uploading the selected image with some custom meta data
final Reference storageReference = storage.ref(fileName);
await storageReference.putFile(
imageFile,
SettableMetadata(customMetadata: {
'uploaded_by': firebaseUser.displayName!,
'description': 'Some description...'
}));
final String downloadUrl = await storageReference.getDownloadURL();
// Refresh the UI
setState(() {});
await photoUploadFirestoreDetails(downloadUrl: downloadUrl);
} on FirebaseException catch (error) {
print(error);
}
} catch (err) {
print(err);
}
}
// photoUploadFirestoreDetails method
Future<Null> photoUploadFirestoreDetails({#required String downloadUrl}) async {
Firebase.initializeApp();
Map<String, dynamic> map = Map();
map['PathImage'] = downloadUrl;
FirebaseFirestore firestore = FirebaseFirestore.instance;
CollectionReference collectionReference =
firestore.collection('MarkerCollect');
var value = await collectionReference.doc().set(map);
}
Try this function to upload image to fire-storage and get Url
Future<String?> uploadAndGetUrl(File file) async {
try {
final Reference ref = FirebaseStorage.instance
.ref()
.child('profilePhoto')
.child(DateTime.now().microsecondsSinceEpoch.toString());
UploadTask uploadTask = ref.putFile(file);
await uploadTask.whenComplete(() {});
String url = await ref.getDownloadURL();
return url;
} catch (e) {
print('Firebase Storage Error is : $e');
return null;
}
}
OR you can just upload an image and get the image URL later.
Your upload image function looks okay. the name should be unique. otherwise, it returns a different image url.
Future<String> getUrl(String imageName) async {
try {
Reference storageRef = FirebaseStorage.instance.ref().child('profilePhoto/$logo');
String url = await storageRef.getDownloadURL();
return url;
} catch (e) {
return null;
}
}

Firebase Stream bug, a custom class rideDetails.dart is not being recognized in the database.dart file

First of all apologies, I didn't know how to phrase the title.
Basically I have this class:
class rideDetails{
final String name;
final String seats;
final String distance;
final String departure;
final String destination;
final String date;
final bool limit;
rideDetails({this.name,this.seats,this.distance,this.departure,this.destination,this.date,this.limit});
}
And I want to use this in my database services dart file, I've included the whole code since I'm not sure what's causing the problem.
import 'package:carpoolapp/models/rideDetails.dart';
import 'package:carpoolapp/models/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart'; // For File Upload To Firestore
import 'package:flutter/material.dart';
import 'package:path/path.dart' as Path;
class DatabaseService{
final String uid;
DatabaseService({this.uid});
// collection reference
final CollectionReference usersCollection = FirebaseFirestore.instance.collection('users');
final Reference firebaseStorageRef = FirebaseStorage.instance.ref();
// images
File _image;
String _uploadedFileURL;
Future updateUserData(String name) async {
return await usersCollection.doc(uid).set({
'name': name,
});
}
Future updateImagePath(String path) async {
return await usersCollection.doc(uid).update({
'path': path,
});
}
Future updateTripDetails(String seats, String date, String distance, bool limit, String departure, String destination) async {
return await usersCollection.doc(uid).update({
'seats': seats,
'date' : date,
'distance' : distance,
'limit' : limit,
'departure' : departure,
'destination' : destination,
});
}
Future getLimit() async {
return await usersCollection.doc(uid).get().then<dynamic>((DocumentSnapshot snapshot) async {
if(snapshot.data()['limit'] == null){
return false;
}
else {
return true;
}
});
}
Future getProfile() async {
return await usersCollection.doc(uid).get().then<dynamic>((DocumentSnapshot snapshot) async {
if(snapshot.data()['name'] == null){
print("No name exists");
}
else {
return snapshot.data()['name'];
}
});
}
// userData from snapshot
UserData _userDataFromSnapshot(DocumentSnapshot snapshot){
return UserData(
uid: uid,
name: snapshot.data()['name'],
);
}
// upload profile picture to firebase
Future uploadPic(BuildContext context, File image) async{
if(image != null) {
String fileName = Path.basename(image.path);
UploadTask uploadTask = firebaseStorageRef.child(uid).putFile(image);
} else{
return null;
}
}
// ride list from snapshot
List<rideDetails> _rideDetailsListFromSnapshot(QuerySnapshot snapshot){
return snapshot.docs.map((doc){
return rideDetails(
name: doc.data()['name'],
date: doc.data()['date'],
departure: doc.data()['departure'],
destination: doc.data()['destination'],
distance: doc.data()['distance'],
seats: doc.data()['seats'],
limit: doc.data()['limit'],
);
}
).toList();
}
Stream<List<rideDetails>> get rideDetails{
return usersCollection.snapshots().map(_rideDetailsListFromSnapshot);
}
// get user doc stream
Stream<UserData> get userData{
return usersCollection.doc(uid).snapshots().map(_userDataFromSnapshot);
}
}
Now the rideDetails class is not being recognized here even after importing it several times. However, the weird thing is when I remove the Stream code, the class is instantly recognized but when I include it, the class isn't recognized. And I do not believe the problem might be with the code itself(Although it could be) because I'm essentially doing the same thing with the UserData class in the same file and that is working perfectly. I've also tried creating the rideDetails class inside the database services file but that isn't solving the problem. I've also restarted android studio several times and commands such as flutter clean. Can anyone help me figure it out?
// ride list from snapshot
List<rideDetails> _rideDetailsListFromSnapshot(QuerySnapshot snapshot){
return snapshot.docs.map((doc){
return rideDetails(
name: doc.data()['name'],
date: doc.data()['date'],
departure: doc.data()['departure'],
destination: doc.data()['destination'],
distance: doc.data()['distance'],
seats: doc.data()['seats'],
limit: doc.data()['limit'],
);
}
).toList();
}
Stream<List<rideDetails>> get rideDetails{
return usersCollection.snapshots().map(_rideDetailsListFromSnapshot);
}

Flutter Web Upload to Firestore

I am having issues with Flutter web and uploading Images to Firestore. I'm pretty sure the issue lies in the Image Picker, as the normal(mobile) image picker does not work for the web. The normal image picker returns a File, but the alternative image_picker_web returns an Image, which gets rejected on upload because it's expecting a Future<File>.
image_picker_web has an alternative to return a Uint8List which I have used, and then converted to a File via dart:html - and uploads fine, but the image is corrupted and not viewable.
Here's what I have done:
On Button Press - Pick Image as Uint8List > Convert to Image, Store in memory and Display on Screen
onPressed: () async {
//Upload Image as Uint8List
imageBytes = await ImagePickerWeb.getImage(asUint8List: true);
//Convert Uint8List to Image
_image = Image.memory(imageBytes);
//Show new image on screen
setBottomSheetState(() {
image = _image;
});
},
Convert Uint8List to File using dart:html File and name as users UID.png (PNG Uploaded)
imageFile = html.File(imageBytes, '${user.uid}.png');
Use Method to upload File
import 'dart:async';
import 'package:firebase/firebase.dart' as fb;
import 'package:universal_html/prefer_universal/html.dart' as html;
String url;
Future<String> uploadProfilePhoto(html.File image, {String imageName}) async {
try {
//Upload Profile Photo
fb.StorageReference _storage = fb.storage().ref('profilephotos/$imageName.png');
fb.UploadTaskSnapshot uploadTaskSnapshot = await _storage.put(image).future;
// Wait until the file is uploaded then store the download url
var imageUri = await uploadTaskSnapshot.ref.getDownloadURL();
url = imageUri.toString();
} catch (e) {
print(e);
}
return url;
}
Call method
location = await uploadProfilePhoto(imageFile, imageName: '${user.uid}');
Add data including Location to Firebase Database
//Pass new user ID through to users Collection to link UserData to this user
await AdminUserData(uid: user.uid).updateAdminUserData(name: userName, email: userEmail, profilephoto: location);
Everything is working OK, just the image seems to be corrupted, it also comes back at almost double the filesize, which obviously means the File isn't coming back as the Image..
This is an old post but in case someone still needs help with this as I have been searching around for hours to figure this out. This is how I am doing it.
Import image_picker_web. I am using version 2.0.3.
Use ImagePickerWeb.getImageInfo on a button ontap listener to get the image info.
var fileInfo = await ImagePickerWeb.getImageInfo;
Show the image using Image.memory in the widget tree. (optional)
Image.memory(fileInfo.data!,width: 180),
Create firebase upload location
final firebasefileLocation = firebaseStorageLocation.child('${DateTime.now()}_${fireInfo.fileName}');
Upload the image to firebase.
await firebasefileLocation.putData(img.data!);
So this is how my file looks to work for both phone and web. There is more information about this and how to select multiple images on the image_picker_web page. You can use the concepts from here to make it crossed-platformed with IOS and Android too.
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker_web/image_picker_web.dart';
class ImagePickerDemo extends StatefulWidget {
const ImagePickerDemo({Key? key}) : super(key: key);
#override
_ImagePickerDemoState createState() => _ImagePickerDemoState();
}
class _ImagePickerDemoState extends State<ImagePickerDemo> {
MediaInfo? _imageInfo;
Future<void> _pickImage() async {
var fileInfo = await ImagePickerWeb.getImageInfo; //get image
if (fileInfo.data == null) return; // user did not choose image.
setState(() {
_imageInfo = fileInfo; // save image
});
}
Future<void> _uploadImage() async {
if (_imageInfo == null) return;
final firebaseStorageLocation =
FirebaseStorage.instance.ref().child('product_images');
final imageInfo = _imageInfo as MediaInfo;
_imageInfo as MediaInfo;
final firebasefileLocation = firebaseStorageLocation
.child('${DateTime.now()}_${imageInfo.fileName!}');
await firebasefileLocation.putData(imageInfo.data!);
final urlToUseLater = await firebasefileLocation.getDownloadURL();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(onPressed: _pickImage, child: Text('Choose Image')),
ElevatedButton(
onPressed: _imageInfo == null ? null : _uploadImage,
child: Text('Upload Image')),
Image.memory(
_imageInfo!.data!,
width: 180,
)
],
);
}
}
I have not tried the alternatives you mentioned, but below has worked for me before on Flutter web and Firebase. The event listener for uploadInput works for most platforms. The last part regarding document.body.append will ensure that it works on Mobile safari as well.
Future<void> _setImage() async {
final completer = Completer<String>();
InputElement uploadInput = FileUploadInputElement();
uploadInput.multiple = false;
uploadInput.accept = 'image/*';
uploadInput.click();
uploadInput.addEventListener('change', (e) async {
// read file content as dataURL
final files = uploadInput.files;
Iterable<Future<String>> resultsFutures = files.map((file) {
final reader = FileReader();
reader.readAsDataUrl(file);
reader.onError.listen((error) => completer.completeError(error));
return reader.onLoad.first.then((_) => reader.result as String);
});
final results = await Future.wait(resultsFutures);
completer.complete(results[0]);
});
document.body.append(uploadInput);
final String image = await completer.future;
widget.newImage = uploadInput.files[0];
// Upload to Firebase
uploadToFirebase(widget.newImage); // This is dart:html File
uploadInput.remove();
}
Then the upload to Firebase Storage:
uploadToFirebase(String imageName, File file) async {
Firebase.UploadTask task = storage.refFromURL('gs://.../images/' + imageName).put(file);
}
For the underlying question of:
"How to upload image bytes to Firebase Storage?"
Here is a possible implementation:
import 'dart:developer';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
/// Opens a file picker and uploads a single selected file to Firebase storage.
/// Returns a download URL if upload is successful or null if the operation is
/// aborted.
///
/// Throws an exception if more than one file is selected or the selected file
/// size exceeds 300KB
Future<String?> pickAndUploadFile() async {
final ref = FirebaseStorage.instance.refFromURL('gs://YOUR-PROJECT.appspot.com');
String? res;
final filePickerRes = await FilePicker.platform.pickFiles();
if (filePickerRes != null) {
if (filePickerRes.count == 1) {
final file = filePickerRes.files.single;
if (file.size > 300000) {
throw Exception('File must be less than 300KB');
}
final upTask = ref.child('uploads/${file.name}').putData(file.bytes!);
final snapshot = upTask.snapshot;
res = (await snapshot.ref.getDownloadURL()).toString();
} else {
throw Exception('only one file allowed');
}
}
log('downloadUrl: $res');
return res;
}
The result (snapshot.ref.getDownloadURL()) is a qualified URL you can use with any image widget that loads a URL.

How to cache Firebase data in Flutter?

In my app I build a list of objects using data from Firebase. Inside a StreamBuilder, I check if the snapshot has data. If it doesen't, I am returning a simple Text widget with "Loading...". My problem is that if I go to another page in the app, and then come back, you can see for a split second that it says 'Loading...' in the middle of the screen, and it is a bit irritating. I am pretty sure it is downloading the data from Firebase, and building the widget every time I come back to that page. And if I don't do the check for data, it gives me a data that I am trying to access data from null.
Is there a way to cache the data that was already downloaded, and if there has been no change in the data from Firebase, then just use the cached data?
Heres a redacted version of my code:
class Schedule extends StatefulWidget implements AppPage {
final Color color = Colors.green;
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
List<Event> events;
List<Event> dayEvents;
int currentDay;
Widget itemBuilder(BuildContext context, int index) {
// Some Code
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
events = new List(snapshot.data.documents.length);
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
events[i] = Event(
name: doc["name"],
start: DateTime(
doc["startTime"].year,
doc["startTime"].month,
doc["startTime"].day,
doc["startTime"].hour,
doc["startTime"].minute,
),
end: DateTime(
doc["endTime"].year,
doc["endTime"].month,
doc["endTime"].day,
doc["endTime"].hour,
doc["endTime"].minute,
),
buildingDoc: doc["location"],
type: doc["type"],
);
}
events.sort((a, b) => a.start.compareTo(b.start));
dayEvents = events.where((Event e) {
return e.start.day == currentDay;
}).toList();
return ListView.builder(
itemBuilder: itemBuilder,
itemCount: dayEvents.length,
);
},
),
);
}
}
You can use the the following code to define the source you want to retrieve data from. This will search either in local cache or on the server, not both. It works for all get() parameters, no matter if it is a search or document retrieval.
import 'package:cloud_firestore/cloud_firestore.dart';
FirebaseFirestore.instance.collection("collection").doc("doc").get(GetOptions(source: Source.cache))
To check if the search has data in cache, you need to first run the search against cache and if there is no result, run it against the server.
I found project firestore_collection to use a neat extension that can greatly simplify this process.
import 'package:cloud_firestore/cloud_firestore.dart';
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_document.dart
extension FirestoreDocumentExtension on DocumentReference {
Future<DocumentSnapshot> getSavy() async {
try {
DocumentSnapshot ds = await this.get(GetOptions(source: Source.cache));
if (ds == null) return this.get(GetOptions(source: Source.server));
return ds;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
}
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_query.dart
extension FirestoreQueryExtension on Query {
Future<QuerySnapshot> getSavy() async {
try {
QuerySnapshot qs = await this.get(GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return this.get(GetOptions(source: Source.server));
return qs;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
If you add this code, you can simply change the .get() command for both documents and queries to .getSavy() and it will automatically try the cache first and only contact the server if no data can be locally found.
FirebaseFirestore.instance.collection("collection").doc("doc").getSavy();
To be sure whether the data is coming from Firestore's local cache or from the network, you can do this:
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
print(doc.metadata.isFromCache ? "NOT FROM NETWORK" : "FROM NETWORK");
In the case you described you are probably going to still see the loading screen when its "NOT FROM NETWORK". This is because it does take some time to get it from the local cache. Soon you will be able to ask for the query's metadata for cases with empty results.
Like others suggested, you can cache the results and you won't see this. First you can try to cache it in the Widget using something like:
QuerySnapshot cache; //**
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
initialData: cache, //**
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
cache = snapshot.data; //**
This will make your widget remember the data. However, if this does not solve your problem, you would have to save it not in this widget but somewhere else. One option is to use the Provider widget to store it in a variable that lives beyond the scope of this particular widget.
Probably not related, but it's also a good idea to move the Firestore.instance.collection('events').snapshots() to initState(), save the reference to the stream in a private field and use that it StreamBuilder. Otherwise, at every build() you may be creating a new stream. You should be ready for build() calls that happen many times per second, whatever the reason.
Using Generics
Appending to #James Cameron's answer above; I found myself in a situation where, said implementation removed my typecast from withConverter. So, the below adds the generic types back into the functions.
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
extension FirestoreDocumentExtension<T> on DocumentReference<T> {
Future<DocumentSnapshot<T>> getCacheFirst() async {
try {
var ds = await get(const GetOptions(source: Source.cache));
if (!ds.exists) return get(const GetOptions(source: Source.server));
return ds;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
extension FirestoreQueryExtension<T> on Query<T> {
Future<QuerySnapshot<T>> getCacheFirst() async {
try {
var qs = await get(const GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return get(const GetOptions(source: Source.server));
return qs;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
use_case.dart
The implementation below would not compile with James example, as DocumentSnapshot<Object?> is not a subset of DocumentSnapshot<UserModel>. So, by adding the generic parameters back in, we can ensure that this extension maintains any type casts.
Future<DocumentSnapshot<UserModel>> userInfo() async {
return await FirebaseFirestore.instance
.doc("${path_to_user_model_doc}")
.withConverter<UserModel>(
fromFirestore: (snapshot, _) => UserModel.fromJson(snapshot.data()!),
toFirestore: (userModel, _) => userModel.toJson(),
)
.getCacheFirst();
}
pubspec.yaml
environment:
sdk: ">=2.17.1 <3.0.2"
dependencies:
cloud_firestore: ^3.1.17

Resources