Flutter/Firestore: Displaying gridtiles in categories - firebase

We are creating a recipe app for a school project.
We are using Dart/Flutter for the language and we have recipes stored in a Firestore DB collection called 'recipes' which have sub-collections of ingredients, comments and method. Inside the ingredients collection, there is a field called 'proteins' that contains an array of proteins (beef, pork, poultry, etc)
I have managed to make one big grid view which displays thumbnails of all the recipes currently stored in the DB, but we want to set them in categories by their proteins. I managed to make the individual lists for the categories but they each contain all of the recipes. I don't know now which direction to go to somehow search through the DB and then display them on the page.
This is the code for the current list that is being created.
I thought about somehow creating a search function that would create an array of document ID's which would be then used in the compiling of the lists, but not sure where to start
I'm just trying to get some nudge in the direction of how it would be done and not the code. The process of it if you will.
Thanks in advance
child: Container(
alignment: Alignment.bottomRight,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 100.0,
child: ListView.builder(
itemCount: 4,
itemBuilder: (context, index) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${categories[index]}',//displays the protein name (beef
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 25.0),
),
),
),
Container(
height: 180.0,
child: StreamBuilder(
stream: firestoreDb,
builder: (
context,
snapshot,
) {
if (!snapshot.hasData)
return CircularProgressIndicator();
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data.docs.length,
itemBuilder: (context, int index) {
return GestureDetector(
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10)),
child: TestGridTile(
snapshot: snapshot.data,
index: index,
),
),
);
});
}),
),
],
),
),
)),
EDIT TestGridTile code as requested
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:youth_food_movement/recipe/ui/ingredients_page.dart';
//card that displays the recipe information
class TestGridTile extends StatelessWidget {
//snapshot of the database
final QuerySnapshot snapshot;
final int index;
const TestGridTile({Key key, this.snapshot, this.index}) : super(key: key);
static String idNumber;
#override
Widget build(BuildContext context) {
//snaphot of the docs
// ignore: unused_local_variable
var snapshotData = snapshot.docs[index];
var docID = snapshot.docs[index].id;
String recipeID = docID.toString();
return Container(
width: 150,//MediaQuery.of(context).size.width,
height: 150,//MediaQuery.of(context).size.height * 0.25,
//get the image URL
child: FutureBuilder(
future: _getImageURL(docID),
builder: (context, snapshot) {
if (snapshot.hasData) {
//return the image and make it cover the container
return GestureDetector(
child: Image.network(
snapshot.data,
fit: BoxFit.fill,
),
onTap: () {
idNumber = recipeID;
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
IngredientsPage(recipeID)));
},
);
} else {
return Container(
child: Center(
child: CircularProgressIndicator(),
));
}
}),
);
}
//method to get the image URL
Future _getImageURL(var docID) async {
//declare and instantiate the firebase storage bucket
final FirebaseStorage storage = FirebaseStorage.instanceFor(
bucket: 'gs://youth-food-movement.appspot.com');
//ref string will change so the parameter will be the jpg ID (maybe)
String downloadURL =
await storage.ref('recipe_images/$docID').getDownloadURL();
return downloadURL;
}
}```
I have attached an image of how it looks currently[![current image][1]][1]
[1]: https://i.stack.imgur.com/HiaYi.jpg

Because the ingredience is a collection we don't get it with the with the initial data. You have two options:
As it is now load all recipes and for each of them the ingrediences and for each category filter out the recipes that don't fit in it and show only those that do fit in. If you would share more code I could help you with it. What is behind TestGridTile.
Load for each category only the recipes that fin it by using a query with where clause and the arrayContaines. But for that the array proteins can't be nested in the ingredience subcollection of the collection you are filtering.
Both of the would be much easier if you would move the proteines array to the doc of the recipes collection. You could do that with the client side code or even with Firebase Cloud Functions.

Related

Firebase doesn't work cause of null-safety (DART/FLUTTER)

I'm using/learning Firebase for my database works. My snapshot's coming like _jsonQuerySnapshot or _jsonDocumentSnapshot. But it had to be QuerySnapshot or DocumentSnapshot. Because of this I have to encode and decode my snapshot for use my datas.
If I'm not using encode decode json I'm getting null or object errors all the time.
Here is my class extends from state
class _MyHomePageState extends State<MyHomePage> {
final _firestore = FirebaseFirestore.instance;
#override
Widget build(BuildContext context) {
CollectionReference moviesRef=_firestore.collection('movies');
DocumentReference babaRef = _firestore.collection('movies').doc('Baba');
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: Text('FireStore Crud'),
),
body: Center(
child: Container(
child: Column(
children: [
StreamBuilder<QuerySnapshot>(
stream: moviesRef.snapshots(),
builder: (BuildContext context,AsyncSnapshot asyncSnapshot){
List<DocumentSnapshot>listOfDocumentSnapshot=asyncSnapshot.data.docs;
return Flexible(
child: ListView.builder(
itemCount: listOfDocumentSnapshot.length,
itemBuilder: (context,index){
Text('${listOfDocumentSnapshot[index].data()['name']}' ,style: TextStyle(fontSize: 24),);
},
),
);
},
),
],
),
),
),
);
}
}
and this is my error .
First of all, check your data is null or not and then use [] on it. Probably, listOfDocumentSnapshot[index].data() is null. If it is null, render another UI such as loading screen. Namely, your loading screen must be showed until reach the data.
for example:
builder: (BuildContext context,AsyncSnapshot asyncSnapshot){
List<DocumentSnapshot>? listOfDocumentSnapshot = asyncSnapshot.data.docs;
if(!listOfDocumentSnapshot.hasData || listOfDocumentSnapshot == null){
return LoadingScreen(); //etc.
}
return Flexible(
child: ListView.builder(
itemCount: listOfDocumentSnapshot.length,
itemBuilder: (context,index){
Text('${listOfDocumentSnapshot[index].data()['name']}' ,style: TextStyle(fontSize: 24),);
},
),
);
},
Futures (asynchronous programmes) need some time to get data and you have to make your UI wait until you get your data. e.g. database connections, read/write somethings to/from somewhere etc.
For more detail you can read this article.

How do I fetch sub collection from firestore using flutter?

I wanna fetch the data inside the Guests like guestName. The code below I used it to fetch collection and it worked, but I have no idea how to fetch subcollection. I been trying but it's not working.
body: CustomScrollView(
slivers: [
SliverPersistentHeader(pinned: true, delegate: SearchBoxDelegate()),
StreamBuilder<QuerySnapshot>(
//I used this to fetch collection but it doesn't work with subcollection
stream: Firestore.instance
.collection("Guests")
.orderBy("Date", descending: true).snapshots(),
builder: (context, dataSnapshot) {
return !dataSnapshot.hasData
? SliverToBoxAdapter(
child: Center(
child: circularProgress(),
),
)
: SliverStaggeredGrid.countBuilder(
crossAxisCount: 1,
staggeredTileBuilder: (c) => StaggeredTile.fit(1),
itemBuilder: (context, index) {
ItemModel model = ItemModel.fromJson(
dataSnapshot.data.documents[index].data);
return sourceInfo(model, context);
},
itemCount: dataSnapshot.data.documents.length,
);
},
),
],
),
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
// this guestName i wanna fetch
model.guestName,
style: TextStyle(
color: Colors.black, fontSize: 14.0),
),
),
],
),
),
To get a CollectionReference for a subcollection, you first need to have a DocumentReference object to its parent document, and then you can call collection() on that.
In your builder you have dataSnapshot.data.documents[index], which gives you a DocumentSnapshot object, from which you can get a DocumentReference by calling reference on it.
So combined that'd be something like this in your builder:
dataSnapshot.data.documents[index].reference.collection("Guests")
I recommend always keeping the reference documentation handy that I linked above, as it's the easiest way to find this type of API path.

Get information from Firebase and able to use it with a Gridtile and GestureDetector in Flutter

So I'm trying to design a Widget which can basically create a view where you can have something like this:
The problem that I'm facing is that for this example I used is calls to API's of Firebase, however, now I'm using the Firebase Auth and Firestore Extentionts and don't want to use the provider package or HTTP request, I have the following Firestore Collection:
As you see the collection name is users but inside has the user uid given by Firebase as a Document and inside that document has all the user Data.
So I need some help to have the following functionalities:
I need to create a ListView preferably ListView Builder so it can show the entire collection so it can display all the users contained in the collection. (Each user is a Document titled with his own User uid).
In the Tile, I want to display just name and Image but once it is clickable it should send me to a new page (the user profile page sending as an argument the uid)
I got this code: but I don't have the logic on how to get the list tile clickable and get the uid of the click user (in the tile) to send the info (uid) to the next page. But once I know how to display the contents of all the documents in the collection (displaying the name (nombre) inside each document as a title and how to capture the uid, I bet I can do the modifications and eliminations.
import 'package:flutter/material.dart';
import 'package:firebase_firestore/firebase_firestore.dart';
class ExpenseList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("users").snapshots,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text("No existe registros de Usuario");
return new ListView(children: getExpenseItems(snapshot));
});
}
getExpenseItems(AsyncSnapshot<QuerySnapshot> snapshot) {
return snapshot.data.documents
.map((doc) => ListTile(title: new Text(doc["nombre"], onTap: (){
//Here function to navigate to profile page saving the uid
}))
.toList();
}
}
In that list tile, I should have a Network image as I can have a parameter the image from the document in the collection.
Is there anything I can do?
Kind Regards.
Here you can find an example of a custom build ListView.
var futureBuilder=new FutureBuilder(
future: makecall.firebaseCalls(databaseReference), // async work
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start');
case ConnectionState.waiting: return new Text('Loading....');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index){
return Card(
elevation: 0.0,
child: Padding(
padding: const EdgeInsets.all(0.0),
child: SizedBox(
height: MediaQuery.of(context).size.height*0.15,
width: MediaQuery.of(context).size.width,
child: Card(
elevation: 0,
child: Row(
children: <Widget>[
new Container(
child: Image.network(snapshot.data[index].iconUrl,height: MediaQuery.of(context).size.width*0.3,width: MediaQuery.of(context).size.width*0.3,),
),
Padding(
padding: const EdgeInsets.only(left: 10,right: 5,top: 5),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
condition('${snapshot.data[index].vegOrNon}')==true? new Image.asset('images/non_veg.png',height: 15,width: 15,): new Image.asset('images/veg.jpg',height: 15,width: 15),
SizedBox(width: 5),
Text(snapshot.data[index].foodtitle, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20,fontFamily: 'Roboto-Black'),)
],
),
SizedBox(height:10.0),
Row(
children: <Widget>[
new IconTheme(
data: new IconThemeData(
color: Colors.black26),
child: new Icon(Icons.timer,size: 20.0,),
),
Text('${snapshot.data[index].PreptimeInMins} minutes',style: TextStyle(fontWeight: FontWeight.w700,color: Colors.black26),),
],
),
],
)
],
),
),
],
),
),
],
)
)
),
),
);
},
);
}
},
);
I found this example on this guide and I think that it can be usefull for you.
If your issue is how to make a ListView tile clickable you can use the GestureDetector class to wrap your tile and send the UID in the .OnTap event

How to get data from two collections in firebase using Flutter

This is my problem:
I have a ListPost StatefulWidget where I want to display a list of widgets that contains the user's account image, the user's name, and the user's posts images(similar to Facebook feeds), however, I have gotten to the point that I need to get that data from two different collections in Firebase (see my firebase collections image below).
The good thing is that I have been able to get that data only from one collection(userFeed) and display that data in my ListPost file in different widgets, however, I do not know how to get data from another collection in Firebase using the same streamBuilder and display all that data I want to display in other widgets in my ListPost screen.
So, my specific question is:
How can I make my ListPost screen to populate data from 2 different collections in Firebase using a stream builder or another type of implementation?
This is the firebase image
This is the complete code for the ListPost screen
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'models/post_model.dart';
final _stream = Firestore.instance.collection('userFeed').snapshots();
class ListPosts extends StatefulWidget {
#override
_ListPostsState createState() => _ListPostsState();
}
class _ListPostsState extends State<ListPosts> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
//this is the Streambuilder to get the data however it only lets me to get one collection
child: StreamBuilder(
stream: _stream,
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
return ListView.builder(
itemExtent: 550.0,
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int data) {
//here I get the data from the userFeed colecction
Post post = Post.fromDoc(snapshot.data.documents[data]);
return Column(
children: <Widget>[
GestureDetector(
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 10.0,
),
child: Row(
children: <Widget>[
CircleAvatar(
radius: 25.0,
backgroundColor: Colors.grey,
backgroundImage: post.imageUrl.isEmpty
? AssetImage(
'assets/images/user_placeholder.jpg')
: CachedNetworkImageProvider(post.imageUrl),
),
SizedBox(width: 8.0),
Text(
post.caption,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
GestureDetector(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
image: DecorationImage(
image:
CachedNetworkImageProvider(post.imageUrl),
fit: BoxFit.cover,
),
),
),
],
),
),
],
);
},
);
},
),
),
);
}
}
UPDATE 05-22-2020 HOW I FIXED THE ISSUE
Credits to the user griffins, he helped me to fix this issue.
This is what I do:
I nested my StreamBuilder so I can use 2 streams at the same time
return StreamBuilder(
stream: _stream,
builder: (context, snapshot1) {
return StreamBuilder(
stream: _stream2,
builder: (context, snapshot2) {
if (!snapshot2.hasData) return const Text('Loading...');
if (!snapshot1.hasData) return const Text('Loading...');
return ListView.builder(
itemExtent: 550.0,
itemCount: snapshot2.data.documents.length,
itemBuilder: (BuildContext context, int data) {
User user = User.fromDoc(snapshot2.data.documents[data]);
Post post = Post.fromDoc(snapshot1.data.documents[data]);
return buildbody(user, post, context);
},
);
},
);
},
);
You can can make you body take a widget ListView and for the Listview children have both your lists.
example
body: ListView(
children: <Widget>[
---list1----
--list2-----
]);
or you can use a custom scroll view
return new Scaffold(
appBar: new AppBar(
title: new Text("Project Details"),
backgroundColor: Colors.blue[800]),
body:
new CustomScrollView(
slivers: <Widget>[
new SliverPadding(padding: const EdgeInsets.only(left: 10.0,right: 10.0,
top: 10.0,bottom: 0.0),
sliver: new SliverList(delegate:
new SliverChildListDelegate(getTopWidgets())),
),
new SliverPadding(padding: const EdgeInsets.all(10.0),
sliver: new SliverList(delegate: new SliverChildListDelegate(
getSfListTiles()
))),
new SliverPadding(padding: const EdgeInsets.all(10.0),
sliver: new SliverList(delegate: new SliverChildListDelegate(
getWorkStatementTiles()
))),
]
)
);
update
from #RĂ©mi Rousselet answer You can nest StreamBuilder
StreamBuilder(
stream: stream1,
builder: (context, snapshot1) {
return StreamBuilder(
stream: stream2,
builder: (context, snapshot2) {
// do some stuff with both streams here
},
);
},
)

Cant get StreamBuilder to display data from cloud firestore

I know I have a connection to the database and no errors are appearing so I'm pretty confused. The title and code should summarize the problem fairly well. Think I'm missing something?
here is the main code that should be displaying cards with titles from firebase
mainList() {
StreamBuilder(
stream: Firestore.instance.collection('Events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('Loading');
} else {
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot userPost = snapshot.data.documents[index];
return Container(
width: MediaQuery.of(context).size.width,
height: 350.0,
child: Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Material(
elevation: 14.0,
shadowColor: Color(0x802196F3),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width,
height: 200.0,
child: Text(
'${userPost['title']}',
))
],
),
),
))),
);
},
);
}
});
}
and here is where the function is called:
lass MyAppmain extends State<MyApp> {
#override
Widget build(BuildContext context) {
var listView = ListView.builder(
itemCount: local.length,
itemBuilder: (BuildContext cnxt, int index) {
return new Text(local[index]);
});
return MaterialApp(
home: PageView(
controller: controller,
children: <Widget>[
//home page---------------------------
Scaffold(
appBar: AppBar(
title: Text(
'Events',
),
elevation: 20,
),
//main list view for the cards
//think I use streambuilder for this so google before starting
body: mainList(),//RIGHT HERE
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(context, NewEventTransition());
},
mini: true,
),
),
//Profile Page-------------------------------
Scaffold(
appBar: AppBar(
title: Text(
'Profile',
),
elevation: 20,
),
),
],
));
}
}
Want a listview of cards holding the titles from firebase (will soon be more than titles but want to get this working first)
This is a common problem.
return ListView.builder(
itemCount: snapshot.data.documents.length, // this line is the culprit!
itemBuilder: (context, index) {
print(snapshot.data.documents.length); // it will print null
.......
}
See, It takes some time to fetch data from firebase. When ListView.builder is called the value of snapshot.data.documents.length is actually null. Tho after few seconds it gets data but till then ListView had built the UI and that's why it's blank. To check the value, you can add a Print statement like shown above.
Now there are few ways to solve this problem:
Make an int variable say totalLength, make a function say setTotalLength which makes a call to Firebase/Firestore database and use setState to assign this value to totalLength and then change that code to this:
itemCount: totalLength,
You should Call setTotalLength in your initState method.
Or, you can change your code to this, But I'm NOT 100% sure that this will work:
itemCount: snapshot.data.documents.length ?? 0 // returns 0 if the value is null, may reload UI again when data comes

Resources