Flutter: Retrieve associated object from Future in FutureBuilder widget - flutter

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!

Related

Displaying friends list using Flutter and Firestore

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);
}

Flutter Firebase Storage Data doesn't show Data immediately

I'm currently working on a project in flutter where I want to store uploaded Data in Firebase Storage. This works fine but now I'm facing a problem with showing the data. I have to do a restart for showing the uploaded Data in my List.
I hope someone can help me with this issue.
onPressed: () async {
FilePickerResult? result = await FilePicker.platform
.pickFiles(allowMultiple: true);
if (result == null) return;
final path = result.files.single.path!;
setState(() {
});
final fileName = result.files.single.name;
storage
.uploadFile(path, fileName)
.then((value) => print('Done'));
},
This is my call function when pressing the button.
Future<void> uploadFile(String destination, String fileName) async {
final User? user = auth.currentUser;
final uid = user!.uid;
File file = File(destination);
try {
await storage.ref('$uid/$fileName').putFile(file);
} on firebase_core.FirebaseException catch (e) {
print(e);
}
}
This is my method for pushing the data into firebase storage.
Container(
width: MediaQuery.of(context).size.width - 15,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: FutureBuilder(
future: _loadImages(),
builder: (context,
AsyncSnapshot<List<Map<String, dynamic>>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Row(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
final Map<String, dynamic> image =
snapshot.data![index];
return Card(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(20)),
elevation: 0,
child: ListTile(
dense: false,
contentPadding: EdgeInsets.all(15),
leading: Image.network(
image['url'],
),
trailing: IconButton(
onPressed: () => _delete(image['path']),
icon: const Icon(
Icons.delete,
color: Colors.red,
),
),
),
);
},
),
),
],
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
),
And this is how I display my files in my app.
Looking forward for some help.
I want to see my data directly when uploading it into Firebase Storage.
If you want to show the newly uploaded image right away, you will have to force a refresh of the code that renders the list of images. If you're using a StatefulWidget to render the images, you could for example call setState() on that widget after the upload has completed. If you're using another state management approach, you'd do the equivalent for that approach.
But note that this will only work on the device of the user who uploaded the image. Any other devices will still only see the new image if they manually restart the rendering of the list of images.
A common way to work around this is to store the list of image URLs in one of Firebase's databases (Realtime Database or Cloud Firestore), and render it from there with a StreamBuilder. The StreamBuilder continues to listen for updates to the database, so if one user's image upload completes and they write the URL/path of the image to the database, that immediately refreshes the list of images not just on their device, but on all other connected devices too.

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 FutureBuilder Snapshot is null but Future Does return data

While working with Flutter for a new application client for Kanboard, I encountered the following problem. I have a FutureBuilder that should return a select dropdown menu with items but, for some reason, the Snapshot data is null, although the Future method does resolves and has data on return.
Full page.dart code here: https://pastebin.com/J48nxsdZ
The block having the problem is the following:
Widget _columnSelect() {
return FutureBuilder(
future: columnProvider.getColumns(task.projectId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
List<DropdownMenuItem<String>> columnList = [];
if (snapshot.hasData) {
columnList.add(DropdownMenuItem<String>(
child: Text('Select Column'), value: 0.toString()));
_columns = snapshot.data;
} else {
columnList.add(DropdownMenuItem<String>(
child: Text('Loading..'), value: 0.toString()));
}
_columns.forEach((column) {
columnList.add(DropdownMenuItem<String>(
child: Container(
child: Text(
column.title,
),
),
value: column.id.toString()));
});
return Container(
// margin: EdgeInsets.only(left: 40.0),
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: DropdownButtonFormField(
icon: Padding(
padding: const EdgeInsets.only(right: 12),
child: Icon(Icons.view_column, color: Colors.blue),
),
items: columnList,
value: _columnId,
decoration: InputDecoration(helperText: 'Optional'),
onChanged: (newValue) {
_columnId = newValue;
},
),
);
},
);
}
This is a duplicate of a widget in the same form for a user dropdown select. The original widget (in the same page) is the following:
Widget _ownerSelect() {
return FutureBuilder(
future: userProvider.getUsers(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
List<DropdownMenuItem<String>> usernameList = [];
if (snapshot.hasData) {
usernameList.add(DropdownMenuItem<String>(
child: Text('Select Owner'), value: 0.toString()));
_users = snapshot.data;
} else {
usernameList.add(DropdownMenuItem<String>(
child: Text('Loading..'), value: 0.toString()));
}
_users.forEach((user) {
usernameList.add(DropdownMenuItem<String>(
child: Container(
child: Text(
user.name,
),
),
value: user.id.toString()));
});
return Container(
// margin: EdgeInsets.only(left: 40.0),
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: DropdownButtonFormField(
icon: Padding(
padding: const EdgeInsets.only(right: 12),
child: Icon(Icons.person, color: Colors.blue),
),
items: usernameList,
value: _ownerId,
decoration: InputDecoration(helperText: 'Optional'),
onChanged: (newValue) {
_ownerId = newValue;
},
),
);
},
);
}
For some reason, the "_columnSelect" AsyncSnapshot is null always, even when the Future method is working fine:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:kanboard/src/models/column_model.dart';
import 'package:kanboard/src/preferences/user_preferences.dart';
class ColumnProvider {
final _prefs = new UserPreferences();
Future<List<ColumnModel>> getColumns(projectId) async {
final Map<String, dynamic> parameters = {
"jsonrpc": "2.0",
"method": "getColumns",
"id": 887036325,
"params": {"project_id": projectId}
};
final credentials = "${_prefs.username}:${_prefs.password}";
Codec<String, String> stringToBase64 = utf8.fuse(base64);
String encoded = stringToBase64.encode(credentials);
final resp = await http.post(
Uri.parse(_prefs.endpoint),
headers: <String, String>{"Authorization": "Basic $encoded"},
body: json.encode(parameters),
);
final decodedData = json.decode(utf8.decode(resp.bodyBytes));
final List<ColumnModel> columns = [];
final List<dynamic> results = decodedData['result'];
if (decodedData == null) return [];
results.forEach((column) {
final columnTemp = ColumnModel.fromJson(column);
columns.add(columnTemp);
});
print(columns);
return columns;
}
}
The output of "print(columns)" returns:
I/flutter ( 9486): [Instance of 'ColumnModel', Instance of 'ColumnModel', Instance of 'ColumnModel', Instance of 'ColumnModel']
I don't know what I'm missing here. The form has 2 users dropdown select (with the original FutureBuilder Widget) which works just fine. The Column widget with the Future Builder is the one with the "null" problem in snapshot.data.
Thank you in advance for your time and support with this!
I just found where the problem was:
In the form page(new Task page), The columnProvider.getColumns(task.projectId)) wasn't executing because the "task.projectId" parameter is a String, but the API needs an int.
I was confused because the method were being called by the previous page (A project Page with all the tasks) and the getColumn's argument was indeed an integer: int.parse(projectId).
The Kanboard API doesn't return an error code if the ID parameter is other than INT with this specific call "getColumns" (for some reason).
Of course, Flutter (or Dart) is waiting for a response from http.post that would never arrive. When comparing the two calls from the two pages, I noticed the difference.
So, in conclusion, I specified the int data type argument in the getColumn definition in order to avoid any confusion:
Future<List<ColumnModel>> getColumns(int projectId) async {
Best Regards!

Flutter future double data issue

I am using Futurebuilder to show List issue is its running function 2 time or 3 times i dont know and showing data 2 times.
My function code
getCustomerList() async {
customerS = [];
print('check running');
final storage = new FlutterSecureStorage();
String uUid = await storage.read(key: "uUid");
CollectionReference _collectionRef =
FirebaseFirestore.instance.collection('Transaction');
QuerySnapshot querySnapshot =
await _collectionRef.get();
// Get data from docs and convert map to List
List allData = querySnapshot.docs.where((element) => element['CustomerID'] == widget.data['customerID'])
.map((doc) => doc.data())
.toList();
print(allData);
print('allData length ${allData.length}' );
for (int i = 0; i < allData.length; i++) {
print(allData[i]);
customerS.add(allData[i]);
}
print(customerS);
print('cus length ${customerS.length}' );
return customerS;
}
My future builder code
FutureBuilder(
future: getCustomerList(),
builder: (context, snapshot) {
print('snapshot length ${snapshot.data.length}');
print(snapshot);
print(snapshot.data);
if (snapshot.hasData)
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
print('List length ${snapshot.data.length}');
return Padding(
padding: const EdgeInsets.only(left: 13, right: 13),
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey, width: .5)),
),
child: Padding(
padding: const EdgeInsets.all(13.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'10th May ',
style: TextStyle(fontFamily: 'PoppinsMedium'),
),
Text(
snapshot.data[index]['give'].toString(),
style: TextStyle(
fontFamily: 'PoppinsMedium',
color: Colors.green),
),
],
),
),
),
);
},
);
else
return Text('Result: ${snapshot.data.body}');
}
),
Issue is i have only 2 arrays in list but its showing for look like its running two time i am also try to clear the array but nothing work.
You can look on this picture
enter image description here
I have only 2 array which are in red circle and its showing double can see in blue circle.
Why you are using customerS I mean its saving data in this by initState and when Future call its double.
Remove it from initState and just simply return allData like this
getCustomerList() async {
customerS.clear();
print('check running');
final storage = new FlutterSecureStorage();
String uUid = await storage.read(key: "uUid");
CollectionReference _collectionRef =
FirebaseFirestore.instance.collection('Transaction');
QuerySnapshot querySnapshot = await _collectionRef.get();
// Get data from docs and convert map to List
List allData = querySnapshot.docs
.where((element) => element['CustomerID'] == widget.data['customerID'])
.map((doc) => doc.data())
.toList();
print(allData);
return allData;
}
From the docs:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
This line:
FutureBuilder(
future: getCustomerList(), <- this line
...
)