I have stored the download links of images on Firestore while the images are in firebase storage.
I am trying to retrieve the links and display them via stream builder but I'm encountering an error.
What can I do to fix this.
StreamBuilder:
StreamBuilder(
stream: db
.collection("Highway Secondary School Announcements")
.doc()
.snapshots(),
builder: (context, snapshot) {
if (snapshot!= null && snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(
child: Text("Snapshot Was Not Retrieved"),
);
}
for (int i = 0; i < snapshot.data.documents; i++) {
listOfUrls.add(snapshot.data.documents[i]['url']);
listOfPics.add(Padding(
padding: const EdgeInsets.only(top: 50, bottom: 50),
child: Image.network(listOfUrls[i]),
));
}
return Container(
child: ListView(
children: listOfPics,
),
);
}),
Error:
The getter 'documents' was called on null.
Receiver: null
Tried calling: documents
If you have doc(), you need to specify which documentation(doc('name')) you want to be read, otherwise, you could remove ".doc()".
Reference:Cloud Firestore
before
db.collection("Highway Secondary School Announcements").doc().snapshots()
after
db.collection("Highway Secondary School Announcements").snapshots()
Second question:
I used "final items = snapshot.data?.docs" to get documents from that snapshot.
Here has a nice example Cloud Firestore flutter
final items = snapshot.data?.docs.reversed;
for ( var item in items!) {
final itemName = item.get('name');
final itemLogo = item.get('logo');
final itemDate = item.get('date');
// String itemDate2 = DateTime.fromMillisecondsSinceEpoch(itemDate).toString();
final itemBubble = _getListItemWidget(
iconName: itemLogo,
titleName: itemName,
subTitleName: DateTime.now(),
scoreKeeper1: scoreKeeper1,
scoreKeeper2: scoreKeeper2,
scoreKeeper3: scoreKeeper3
);
itemBubbles.add(itemBubble);
}
Related
I'm having struggles for the last couple of days, so any help highly appreciates it.
I have an app where everyday users take photos of themself ( I set the date of that day as docId), then in UI, every day has a page ( a carousel) where users can swipe and see the photos belonging to every day.
I attached a screenshot of the Firstore database.
But having a problem reading images , tried every method.
P.s : When I set the DocId for instance: 2023-01-11 it works but it just show the photos of one day , I need to fetch all images from all days.
Method adding data to Firestore:
final photoToDb = db
.collection('photos')
.doc(DateFormat('yyyy-MM-dd').format(newDate))
.collection('Today Photos')
.withConverter(
fromFirestore: PhotoModel.fromFirestore,
toFirestore: ((PhotoModel photoModel, options) =>
photoModel.toFirestore()),
);
photoToDb.add(photo);
} catch (e) {
return ('errro');
}
}
Page where I'm trying to display images ,
lass SugarPhotoPage extends StatefulWidget {
const SugarPhotoPage({
super.key,
});
#override
State<SugarPhotoPage> createState() => _SugarPhotoPageState();
}
class _SugarPhotoPageState extends State<SugarPhotoPage> {
final Stream<QuerySnapshot> _photoStream = FirebaseFirestore.instance
.collection('photos')
.doc()
.collection('Today Photos')
.snapshots();
#override
void initState() {
print('${AppData.userSelectedData}');
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: StreamBuilder<QuerySnapshot>(
stream: _photoStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
if (snapshot.hasData) {
return SafeArea(
child: Center(
child: ListView(
children: snapshot.data!.docs
.map((DocumentSnapshot documentSnapshot) {
Map<String, dynamic> data =
documentSnapshot.data()! as Map<String, dynamic>;
return Container(
height: 200,
width: 100,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('${data['ImgUrl']}'),
fit: BoxFit.contain,
),
),
);
}).toList(),
),
),
);
}
return const Text('Loading');
}),
);
}
}
You can't do that with this data structure. Firebase queries are shallow, meaning that you can't query a document together with the documents in sub collections.
In StreamBuilder you can get snapshots of either one specific document by setting :
FirebaseFirestore.instance
.collection(...)
.withConverter<...>(...)
.doc(...)
.snapshots()
or multiple documents:
FirebaseFirestore.instance
.collection(...)
.withConverter<...>(...)
.where(...)
.orderBy(...)
.limit(...)
.snapshots()
In both cases, you will get the data of one or more documents, but if you need the documents in a sub collection, you need to perform another query. For example if you have one document in doc variable, and you need data in its Today Photos sub collection, you need another stream:
doc.collection('Today Photos')
.withConverter<...>(...)
.snapshots()
So with the current data structure you can query into a StreamBuilder all documents in the user's photos collection, but the contents of Today Photos sub collection must be queried separately for each retrieved document of photos collection.
The other option is to change your data structure. You can add the daily photos to the photos collection, let Firebase assign an id to them and add the date as a field. This way you can have one stream for the photos, order them by date, add a limit etc.
I'm trying to build a flutter view that loads a list of items ('cost codes' in the code snippet) from a database call. This code works elsewhere in my project where I already have data in the database, but it fails when it tries to read data from an empty node. I can provide dummy data or sample data for my users on first run, but they might delete the data before adding their own, which would cause the app to crash the next time this view loads.
What's the proper way to deal with a potentially empty list in a StreamBuilder?
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: dbPathRef.onValue,
builder: (context, snapshot) {
final costCodes = <CostCode>[];
if (!snapshot.hasData) {
return Center(
child: Column(
children: const [
Text(
'No Data',
style: TextStyle(
color: Colors.white,
),
)
],
),
);
} else {
final costCodeData =
// code fails on the following line with the error
// 'type "Null" is not a subtype of type "Map<Object?, dynamic>" in type cast'
(snapshot.data!).snapshot.value as Map<Object?, dynamic>;
costCodeData.forEach(
(key, value) {
final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
);
return ListView.builder(
shrinkWrap: false,
itemCount: costCodes.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
costCodes[index].name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
costCodes[index].id,
style: const TextStyle(color: Colors.white),
),
);
},
);
}
},
);
}
Personally I tend to avoid handling raw data from a database in the UI code and handle all of this in a repository/bloc layer.
However, to solve your issue you can simply add a ? to the end of the cast like so:
final costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
You will no longer get the cast exception - however you still have to test costCodeData for null.
This block of code may help:
final data = snapshot.data;
final Map<Object?, dynamic>? costCodeData
if (data == null) {
costCodeData = null;
} else {
costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
}
if (costCodeData == null){
// Show noData
} else {
// Show data
}
final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
you declaired dataLast with a Map having key as String, but inside the account variable the id and name are not in the string format, keep those inside "" || '' even after modiying these, if you still face other issue try putting question mark at the end of the line
(snapshot.data!).snapshot.value as Map<Object, dynamic>?
I am trying to receive data using a FutureBuilder, but it hangs on the CircularProgressIndicator. I think it's remaining on ConnectionState.waiting but I'm not sure why.
#override
initState() {
_widgetList = getWidgetList();
}
Stream<List<String>> getFriendUUIDS() => Firestore.friends
.doc(gameManager.myself.uuid)
.snapshots()
.map((snapshot) => ((snapshot.data()?.keys)?.toList()) ?? []);
Future<List<MomentWidget>> getWidgetList() async{
List<MomentWidget> widgetList = [];
Set<String> momentIds = Set();
await for (final uuids in getFriendUUIDS()){
for (String uuid in uuids){
DocumentSnapshot<Map<String, dynamic>> values = await Firestore.users
.doc(uuid)
.get();
for (String momentId in values.data()?['moments'] ?? [] ){
momentIds.add(momentId);
}
}
}
for (String momentId in momentIds){
DocumentSnapshot<Map<String, dynamic>> values =
await Firestore.instance.collection('moments').doc(momentId).get();
Map<String, dynamic>? data = values.data()!;
String downloadURL = await storage.ref('moments/$momentId').getDownloadURL();
MomentWidget widget = MomentWidget(numLikes: data['liked_by'].length ,
location: data['location'],
date: data['timestamp'],
names: data['taggedFriends'].toString(),
shotBy: data['taken_by'], image: NetworkImage(downloadURL));
widgetList.add(widget);
}
return widgetList;
}
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
height: size.height,
width: size.width,
child: FutureBuilder(
future: _widgetList,
builder: (context, AsyncSnapshot<List<MomentWidget>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, pos) {
return snapshot.data![pos];
},
);
}
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
default:
return Text('Unhandled State');
}
}
),
);
}
I have tried to get the Future inside of initState(), and have tried to use snapshot.hasData instead, to no avail.
I have encountered a similar problem. When building an object from json , if the types don't match , it can quietly fail. I do not think your widgetList is ever returned. In my case I had a variable "cost" that I thought would be of type int , however in the database it was of type String. It always quietly failed and never showed the markers on the map widget
So:
Check how many times that loop of yours is executed. Probably only once and then it quietly fails
If the above happens:
Makes sure the types of your variables match the ones from the database. Comment out every variable one by one to find where the problem is.
Let me know if it works
I'm trying to display an message, but unfortunately I think I'll need to change my method.
So I've resumed some code
child: FutureBuilder<List<Product>>(
future: products,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final products = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(10.0),
child: ListView.builder(
itemCount: products.length, // currently has 400 items
itemBuilder: (context, index) {
if (difference == 0) {
return cardUI(
...
);
} else {
return Text('Nothing to display!');
}
}));
})),
How can I manage to return the message only one time? Do I need to change all the code? Since it's displaying almost 250 times 'Nothing to display'
Edit:
This is what I'm using to calculate the difference!
DateTime productExpire = DateTime.parse(products[index].date);
final productDate = productExpire;
final today = DateTime.now();
final difference = calcDays(
today, productDate);
The solution that comes to mind is to make products only equal to the products that have a difference of days from today of zero, so then based on products.length you can either return a Text() (if products.length == 0) or call ListView.builder (if products.length > 0).
Basically:
Instead of this:
All products
products = [thisProdHasDifferenceOfZero, thisOneDoesnt, thisOneDoes, thisOneDoesnt, ...]
(products.length == 400 every time)
You can just have:
Only products that you want to work with
products = [thisProdHasDiffOfZero, thisOneToo, thisOneToo, ...]
(products.length <= 400)
In your code:
Instead of calculating the difference your way, use this:
This method calculates the difference between two dates, the one you're using may run into some bugs... check this answer for more information
int daysBetween(DateTime from, DateTime to) {
from = DateTime(from.year, from.month, from.day);
to = DateTime(to.year, to.month, to.day);
return (to.difference(from).inHours / 24).round();
}
Then:
child: FutureBuilder<List<Product>>(
future: products,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final today = DateTime.now();
final products = (snapshot.data! as List<Product>).where((product) => daysBetween(product.date, today) == 0).toList;
// Now you know that every product inside 'products', if there's any, has a day difference of 0.
return Padding(
padding: const EdgeInsets.all(10.0),
// 'products' can be empty since you may have 0 products that have the difference you are looking for.
// In that case you return the text.
child: (products.length == 0) ?
return Text('Nothing to display!');
: return ListView.builder( // If products has one or more items...
itemCount: products.length,
itemBuilder: (context, index) {
return cardUI(
...
);
}
)
...
You want to show a single error if the listview.builder does not have any data right? Hope I am not getting you wrong. If so you can use the ternary operator where you have mentioned the listview.builder to make it conditional.
or you can try to update the item count with condition.
itemcount: difference ==0? products.length : 1,
After getting all products filter them and create another list of product that will satisfy difference==0.
final products = snapshot.data!;
List<Product> temp = [];
// temp = products.where((p) => validation(p) == 0).toList();
for(final p in products)
{
int difference = yourValidation(p);
if(difference == 0) temp.add(p);
}
//...
child: ListView.builder(
itemCount: temp.length,
More about List
I am trying to get a specific icon to appear based on whether the user has taken the survey or not.
At the moment, I am using StreamBuilder<DocumentSnapshot> to listen for a given value in a document, which returns the survey name. I then want to use the survey name in the next StreamBuilder, which will look in a given collection (which is made up of the survey name followed by _entrants - so, for example, Survey_entrants) for a completed survey document, which will have the title of the user's unique id (named userid).
The problem I have now is that whilst surveyName does return the name of the survey put in Cloud Firestore, and updates it when I change the value (I can see this by the commented-out return new Text('$surveyName'); command).
However, it does not seem to be passing that value into the next StreamBuilder - regardless of what I put in as the survey name, I get the check icon showing, suggesting (snapshot1.hasData) - even when that document does not exist.
I know the surveyName variable is working, but if I do snapshot1.toString() I get the error Snapshot<DocumentSnapshot>(ConnectionState.active, Instance of 'DocumentSnapshot', null). This must count has having data, hence showing the survey being taken. How do I correct this?
My code:
Positioned(
right: 30,
top: 20,
child: StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance
.collection('Controller')
.document('Current Survey')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
} else {
var sn = snapshot.data;
surveyName = sn["cs"];
// return new Text('$surveyName');
return StreamBuilder(
stream: Firestore.instance
.collection('$surveyName' + '_entrants')
.document(userid)
.snapshots(),
builder: (BuildContext context, snapshot1) {
if (!snapshot1.hasData) {
return Icon(
Foundation.burst_new,
size: 48,
color: Color(0xff303841),
);
} else if (snapshot1.hasData) {
return Icon(
Foundation.check,
size: 48,
color: Color(0xff303841),
);
} else {
return Icon(
MaterialIcons.error_outline,
size: 48,
color: Color(0xff303841),
);
}
});
}
})),