Displaying friends list using Flutter and Firestore - flutter

I have been scowering the internet and trying to find a way to structure and builda friend system using Flutter and Firebase. I have settled on the following structure, but am certainly open to new suggestions:
I have a collection containing all existing users. Where each document is the users uid. Furthermore each document again has a few collections containging the friend data. I keep track of which users have sent the user in question a friendrequest, and which friend requests have been sent out by the user in question. Last but not least ofcourse a list of users which are actually his friends.
Initial sctructure
All 3 of these sub collection simply hold more uid's, since I dont want to store a copy of the actual user data here. sub collection structure. This is because when a user updates his personal information I have to also update all of the information which belong to all the friends, resulting in a huge number of reads and writes. So with these uid's I want to backtrack to the initial user collection to find his/her information and display it
So my idea was to have a StreamBuilder which has a snapshot of all the friends of the current user. These snapshots can then be used to track back to the user collection and fetch that user's data. However I have many doubts on this structure since it isnt very asynchronous and is giving me errors. Because the index of the Listview is continuing onward while the data isnt loaded yet.
And I want them added to a list so I can manage the state of the isSelected variable and the ProfilePicture. Also if there is a better way of doing this please enlighten me!
I know it is quite the long question, but could somebody please help me out on this one :)
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.collection('friends')
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshots) {
if (snapshots.hasError) {
return const Text("Er is iets fout gegaan!");
}
if (snapshots.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshots.data!.docs.isEmpty) {
return Column(children: const [
Icon(
Icons.info,
color: Colors.grey,
size: 50,
),
Text(
"Voeg eerst vrienden toe!",
style: TextStyle(color: Colors.grey, fontSize: 26),
)
]);
}
return Expanded(
child: ListView.builder(
itemCount: snapshots.data!.docs.length,
itemBuilder: (context, index) {
return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance
.collection("users")
.doc(snapshots.data!.docs[index].id)
.get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data =
snapshot.data!.data() as Map<String, dynamic>;
allReceiptDebtors.add(ReceiptDebtor(
snapshots.data!.docs[index].id,
data['email'],
data['username'],
ShowProfilePicture()
.show(data['image'], data['username']),
false,
false));
print(index);
print(allReceiptDebtors);
return ListTile(
title: Text(
allReceiptDebtors[index].username,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.black54,
fontSize: 16,
fontWeight: FontWeight.bold),
),
subtitle: Text(
allReceiptDebtors[index].email,
overflow: TextOverflow.ellipsis,
),
leading: Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: allReceiptDebtors[index].profilePicture,
),
trailing: Visibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
visible: allReceiptDebtors[index].isSelected,
child: const Icon(
Icons.check_rounded,
color: SplitlyColors.gold,
),
),
onTap: () {
setState(() {
if (!allReceiptDebtors[index].isSelected) {
allReceiptDebtors[index].isSelected = true;
} else {
allReceiptDebtors[index].isSelected = false;
}
});
},
);
}
return Container();
},
);
}));
});
class ReceiptDebtor {
final String docId;
final String email, username;
final Widget? profilePicture;
List items = [];
bool isSelected;
bool isExpanded;
ReceiptDebtor(this.docId, this.email, this.username, this.profilePicture,
this.isSelected, this.isExpanded);
}

Related

.where() not working with StreamBuilder steam | firestore query require an index - Flutter

I want to display groups with this condition
current user id must include in the members array of group collection
order by LastUpdate (time)
So, I used steam like this
StreamBuilder(
stream: FirebaseFirestore.instance
.collection("groups")
.orderBy('LastUpdate', descending: true)
.where(
"members",
isEqualTo: FirebaseAuth.instance.currentUser!.uid,
)
.snapshots(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: const TextStyle(color: Colors.white),
);
}
if (snapshot.hasData) {
return Padding(
padding: const EdgeInsets.only(top: 5),
child: ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, index) {
return Text(
snapshot.data.docs[index]['groupName'],
style: const TextStyle(fontSize: 40, color: Colors.white),
);
},
),
);
} else {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
});
When I run first time, It showed error and said,
FAILED_PRECONDITION: The query requires an index. You can create it here: ...
I created it. there is picture of it,
With this, Application run without error. But,
When I use .where( "members",isEqualTo: FirebaseAuth.instance.currentUser!.uid,), It not display anything. empty without any error.
(There are 3 groups, this user id has in the member's array - its sure. I checked it correctly)
When I remove .where( "members",isEqualTo: FirebaseAuth.instance.currentUser!.uid,) from snapshot. It working properly.
What can I do to fix this. HELP
If members is an array in a document, you must use "arrayContains" not "isEqualTo"
.where(
"members",
arrayContains: FirebaseAuth.instance.currentUser!.uid,
)

How to display all data in array?

I'm trying to display data from an array in firestore. I displayed it, but only [0] in the array is showing. I'm trying to get all the data in the array to show.
builder: (_, AsyncSnapshot<List<DocumentSnapshot>> snapshot){
if(snapshot.hasData){
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: ((_, index) {
List<Widget> tiles = [];
for (Map post in snapshot.data![index]['posts']) {
tiles.add(
Expanded(
child: Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(1),
decoration: BoxDecoration(border: Border.all(color:Colors.black)),
child: Center(
child: ListTile(
title: Text(post['postText'], style: TextStyle(color: Colors.white),),
subtitle: Text(post['fromUser'], style: TextStyle(color: Colors.white),),
),
),
),
)
);
}
return Expanded(
child: ListView(
children: tiles,
),
);
}),
);
}
else{
return Center(child: CircularProgressIndicator(),);
}
},
enter image description here
Edit
To answer your qn about newest to oldest:
I suggest you put a FieldValue.timestamp field in your group chat documents! Then, you can order them like this:
Future<List<DocumentSnapshot>> getDoc(groupID) async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection('groups')
.where('groupChatId', isEqualTo: groupID)
.orderBy('timestamp', descending: true) // <- Here!
.get();
return qn.docs;
}
(All of that I copied by hand, since you hadn't provided this code as text, as I asked you to!... 😆)
If you don't have a timestamp field, there is a way to still find out when a document was created... but I don't know how. Plus, in this case, I guess you want the time a certain FIELD was created in the document...! I don't know if that's possible. In fact, for that you'll probably have to do:
List<Map> posts = snapshot.data![index]['posts'];
// Sort list according to the 'date' field in each Map in the list:
posts.sort((mapA, mapB){
return mapA['date'].compareTo(mapB['date']);
});
// Then you'll use posts in your for-loop instead of snapshot.data![index]['posts']:
for (Map post in posts) {
tiles.add( /*etc*/);
}
Btw, if you want it to update when new messages come in, you can do like this:
import 'dart:async';
// Put the below in the State of a StatefullWidget:
StreamSubscription<QuerySnapshot<Map<String, dynamic>>>? qn;
List<DocumentSnapshot>? eventDocs;
void getDocStream(groupID) async {
var firestore = FirebaseFirestore.instance;
qn = firestore.collection('groups')
.where('groupChatId', isEqualTo: groupID)
.orderBy('timestamp', descending: true)
.snapshots().listen((event) {
// Put here everything you want to happen when new things happen in the stream!
// For example:
setState(() {
eventDocs = event.docs;
});
// Now, you can use eventDocs instead of getDoc(groupID), as you did before.
// Just remember that it will be null at first!
});
}
#override
void dispose() {
if (qn != null) qn!.cancel(); // This is to prevent the stream from going on, after you've left the page or even closed the app...
super.dispose();
}
Old answer:
But you're telling it to display only post [0]!...
If there are more posts in each document, and you want to display all of them, you need to make a for-loop or something. For example:
itemBuilder: ((_, index) {
List<Widget> tiles = [];
for (Map post in snapshot.data![index]['posts']) {
tiles.add(
ListTile(
title: Text(post['postText']),
subtitle: Text(post['fromUser']),
));
}
return Expanded(
child: Column(
children: tiles,
),
);
}),
And btw... Next time you ask a qn, plz paste your code as text rather than an image! So that we can copy-paste it into our answer, rather than having to retype it from the image. It's so easy to make a mistake and then you get an error coz we didn't copy it right.
try this
title: Text(snapshot.data![index]['posts']['postText']),

How do I setup a scoreboard in flutter? (using firebase)

Hi I'm having trouble setting up my scoreboard in flutter, everything is working as intended except one part. I want to create a scoreboard which displays a username, image and some data. All the data is stored in a collection in firebase and I have set it up as following.
date "2022-05-12"
time 20220512
trÀning "data"
userID "x"
I would like to get the username and the image from another collection where I've saved the user info. The user documents looks like this and they are named after the userID.
name "x"
imagePath "x"
My original idea was to get the data and the userID and then use the userID to get the name and image. This is my current code. Im getting the correct userID but I cant figure out how to get the name and the image from the user documents. Does anyone know how I could solve this? I'll add an example screenshot at the end. Thanks for reading.
StreamBuilder<QuerySnapshot<Object?>> buildStreamBuilderTop() {
return StreamBuilder<QuerySnapshot>(
stream: trViewTop,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('NĂ„tt gick snett');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Datan hÀmtas');
}
final data = snapshot.requireData;
return AnimationLimiter(
child: ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
QueryDocumentSnapshot user = snapshot.data!.docs[index];
String userID = user['userID'];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(seconds: 1),
child: SlideAnimation(
verticalOffset: 44,
child: FadeInAnimation(
child: Card(
child: ListTile(
leading: CircleAvatar(radius: 30, backgroundImage: NetworkImage(*this is where i want the image*)),
title: Text(*this is where i want the name*),
subtitle: Text('TrÀningar ${data.docs[index]['totalTr']}'),
trailing: Text(' + ${data.docs[index]['totalTr'] * 20 } Kr',
style: TextStyle(color: Colors.green),),
),
),
),
),
);
}
),
);
},
);
}

Flutter - How to get the value of a provider call function that requires 'await' within a variable?

I'm trying to make a budget app where each budget has its own spending history. Each of those spending histories would have a variable called 'budgetName' which I can compile and total the amount of spending by using sqflite code as below.
return await db.rawQuery("select sum(budgetSpent) as total from spending where budgetName ='" + budgetTitle + "'");
and this works if I try to use a .then((value) {print(value);}) when calling the sqflite function and see the value of each budget's spendings in the debug console.
But the problem is that I need the 'budgetTitle' when calling the function so it can compare with the spending's 'budgetName' to get the total spending amount.
So what I have right now is I try to get the spending amount like below:
child: BudgetCard(
budgetName: budget.budgetName,
budgetSpent: '${Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName}',
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5),
),
But it only returns Instance of 'Future<dynamic>' because it needs the 'await' before getting the value. But I couldn't find another way of doing this because it needs the 'budgetTitle' to be passed on.
Any help, ideas, or suggestions are highly appreciated! thank you in advance.
Here is the database code:
String? budgetSpendingAmount;
getSpecificSpending(budgetTitle) async {
dynamic result =
await SpendingDatabaseHelper.instance.getSpendingAmount(budgetTitle);
String a = result.toString();
debugPrint('A: $a');
if (a == '[{total: null}]') {
a = currency.format(int.parse('000'.trim()));
budgetSpendingAmount = a;
print(budgetSpendingAmount);
} else {
String? b = a.replaceAll(RegExp(r'[{\}\[\]\-]+'), '');
String c = b.substring(b.indexOf(":") + 1);
budgetSpendingAmount = currency.format(int.parse(c.trim()));
}
notifyListeners();
}
Future getSpendingAmount(String budgetTitle) async {
Database db = await instance.database;
return await db.rawQuery("select sum(budgetSpent) as total from spending where ='" + budgetTitle + "'");
}
Here is the full code of where I call the function to get the spending amount data:
Widget build(BuildContext context) {
return FutureBuilder<List<Budget>>(
future: Provider.of<BudgetDatabaseHelper>(context).getBudgets(),
/// Displaying the data from the list
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center();
}
return snapshot.data!.isEmpty
? const Flexible(
child: Center(
child: Padding(
padding: EdgeInsets.only(bottom: 80.0),
child: Text(
'You don\'t have any budget',
style: kCaption,
),
)))
: Flexible(
child: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final budget = snapshot.data![index];
return Dismissible(
key: UniqueKey(),
background: const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(bottom: 12.0, right: 24),
child: Icon(
IconlyLight.delete,
color: cRed,
size: 24,
),
),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
snapshot.data!.removeAt(index);
Provider.of<BudgetDatabaseHelper>(context,
listen: false)
.removeMethod(budget.id!, budget.budgetName);
},
child: GestureDetector(
onTap: () => showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
enableDrag: true,
isScrollControlled: true,
builder: (context) {
return DraggableScrollableSheet(
snap: true,
minChildSize: 0.43,
maxChildSize: 0.85,
initialChildSize: 0.43,
snapSizes: const [0.43, 0.85],
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32)),
child: Container(
color: cWhite,
child: SingleChildScrollView(
controller: scrollController,
physics: const BouncingScrollPhysics(),
child: BudgetDetails(
id: budget.id!,
budgetName: budget.budgetName,
budgetSpent: 'budgetSpent',
colorValue:
colorSwatch[budget.colorValue],
maxBudget: currency.format(
int.parse(budget.maxBudget)),
svgIcon: iconListBudgetDetails[
budget.iconValue],
),
),
),
);
},
);
},
),
child: BudgetCard(
budgetName: budget.budgetName,
budgetSpent: '${Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName}',
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5),
),
);
},
),
);
},
);
}
Use provider in a widget tree is not a good idea. Make a statefullWidget
Make a getter in your SpendingDatabaseHelper like this
String? _budgetSpendingAmount;
String get budgetSpendingAmount=> _budgetSpendingAmount;
and initialize it like this _budgetSpendingAmount = currency.format(int.parse(c.trim()));
So using this getter you can access this value anywhere in widget tree
Future<void> _getSpecificSpending(String budgetName)async{
try{
await Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budgetName);
} catch(e){
print('error :$e');
}
}
and in your widget tree write something like this
child: FutureBuilder(
future : _getSpecificSpending(budget.budgetName)
builder: (ctx,snapshot){
var spendDataProv=Provider.of<SpendingDatabaseHelper>(context, listen: false);
return snapshot.connectionState==ConnectionState.waiting ?
CircularProgressIndicator() :
BudgetCard(
budgetName: budget.budgetName,
budgetSpent:spendDataProv.budgetSpendingAmount ,
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5)
},
)
Some idea's
Use a FutureBuilder inside your BudgetCard widget. You can then show a CircularProgressIndicator where the spent amount is going to be when you are still waiting on the future to finish.
Or
Use a Boolean flag (which you flip at the beginning of the future method and at the end) that indicates whether the future is finished. Flag false: show progressIndicator, flag true show the spent amount.
Or
When calling Provider.of<BudgetDatabaseHelper>(context).getBudgets() you can let the method getBudgets() also fill an array with the information you need later on. So, call Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName) inside the getBudgets() method for each budgetName you have.

Flutter: Retrieve associated object from Future in FutureBuilder widget

I am fetching the user 'event manager id' data coming from a future of the object 'event'. I would like now to fetch a user using that id to display his name next the event. However, my FutureBuilder widget only takes into account one future (Event) and I am not able to retrieve that user's name based on that event since my fetchUser method will only return Future objects.
Any help is greatly appreciated.
Here's the FutureBuilder widget:
body: new FutureBuilder(
future: events,
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
List<Event> availableEvents = snapshot.data;
if (!snapshot.hasData) return CircularProgressIndicator();
return new ListView.builder(
scrollDirection: Axis.vertical,
padding: new EdgeInsets.all(6.0),
itemCount: availableEvents.length,
itemBuilder: (BuildContext context, int index) {
user = fetchUserbyId( // Here, user is of type Future<user> and I cannot retrieve info such as the name of that user
(availableEvents[index].managerId).toString());
return new Container(
margin: new EdgeInsets.only(bottom: 6.0),
padding: new EdgeInsets.all(6.0),
color: Colors.white,
child: Column(
children: <Widget>[
new Text('${availableEvents[index].name}',
style: TextStyle(
fontWeight: FontWeight.bold,
height: _height,
fontSize: 18)),
new Text('${availableEvents[index].description}',
style: TextStyle(height: _height)),
new Text('${availableEvents[index].address}',
style: TextStyle(height: _height)),
new Text('${availableEvents[index].datetime}',
style: TextStyle(height: _height)),
//new Text('${availableEvents[index].managerId}', style: TextStyle(height: _height)),
new FlatButton(
onPressed: null,
// Simply call joinEvent for event 'availableEvents[index]'
color: Colors.redAccent,
textColor: Colors.white,
disabledColor: Colors.red,
disabledTextColor: Colors.white,
padding: EdgeInsets.all(8.0),
splashColor: Colors.redAccent,
child: Text('Join!'),
)
],
));
},
);
}));
Here is the fetchUserByID method:
Future<User> fetchUserbyId(String id) async {
final response =
await http.get('https://url-here.com' + id);
//print("response : " + response.body);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON.
return User.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
If what I'm understanding is you have two asynchronous calls, where the second one needs the results of the first call to execute. The best way to go around this is to create a helper method, i.e. getData(). In this method you make your call to events and then use that to fetchUserbyId. This would result in your FutureBuilder looking something like this:
FutureBuilder(
future: getData()
builder: ... // get the results the same why you got your results from events in the given example.
);
Then in you getData() method it would look something like this:
Future<User> getData() async {
var availableEvents= await events; // not sure what your events data/method is
return fetchUserbyId((availableEvents[index].managerId).toString());
}
I think I answered your question, but if I missed it please comment.
Note: On a completely unrelated topic, you don't need the new keyword in Flutter anymore to instantiate objects. Hope that speeds up your development process!