I am making a shopping app. In the products i have given a few tabs of different types of products which are being received from the api. I want to add another tab of 'All' which displays all the products in all other tabs together. All the APIs are being called with id of each tab title and are identical in structure. Online i could find a few answers regarding future.wait but i don't think it will work in this case because they talk about completing one task after another which is what i want but i want to combine the results too. Basically, how to combine all of these identical futures into one single future to be passed into futurebuilder or is there a workaround to this problem?
Edit:
I am getting my List of futures like this:-
for (int i = 0;i <Length;i++) {
ProductsList.add(CollectionProductServices().getCategories(collectionModel.childPackages[i].id.toString()));
}
Your solution is using Future.wait :
FutureBuilder(
future: Future.wait([
firstFuture(), // Future<List<product>>
secondFuture(),// Future<List<product>>
//... More futures
]),
builder: (
context,
// results of all futures above
AsyncSnapshot<List<List<product>>> snapshot,
){
// Check hasData once for all futures.
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
// Merge lists into one list
final List<product> result = [...snapshot.data[0], ...snapshot.data[1]];
return Container();
}
);
you could simply use wait and expand
final ress = await Future.wait([
for (int i = 0;i <Length;i++)
CollectionProductServices().getCategories(collectionModel.childPackages[i].id.toString());
]);
final allProduct = ress.expand((element) => element).toList();
and you could use map insted of for like
final categoriesFuture = Future.wait<List>(
collectionModel.childPackages.map((item)=> CollectionProductServices().getCategories(item.id.toString()) );
);
but i don't things this is a good solution as we get each Categories twice one for it's tab and other for all Categories tab
you should get it only once and combine the same result somethings like
then use categoriesFuture,allProduct for displaying
// inside initState or class
final List<Future> categoriesFuture =
collectionModel.childPackages.map((item)=> CollectionProductServices().getCategories(item.id.toString()) );
final allProduct = Future.wait(categoriesFuture).then((value) => value.expand((element) => element).toList()) ;
//inside build method:
Column(
children: [
for (var item in categoriesFuture)
FutureBuilder(future: item,)
]);
FutureBuilder(future: allProduct,)
Related
I am having trouble getting a stream with a list of objects to populate in a ViewModel.
Load an asynchronous Stream<List<Habit>> from a Firestore Service
in a DailyViewModel.
Call a transform method to turn that stream into a Stream<List<HabitCompletionViewModel>> that looks at a specific
instance of each of my habits for today, and creates that instance if one doesn't exist. There are a few component pieces to this:
For each habit in the initial stream, run a private method that checks if there is an instance of the habit for today, and initializes one if not. This is an asynchronous method because it calls back to the database to update the habit with the new instance.
Then find the instance for today, and return a HabitCompletionViewModel with the habit and today's instance.
Map these to a list.
That new stream is set in a property in the DailyViewModel as todaysHabits.
todaysHabits is called as the stream in a StreamBuilder in the DailyView widget.
The issue I am running into is that I know a completion for today is being found.
I am very fuzzy on what can/should be called as asynchronous code, and whether I'm using correct async/async* return/yield statements in my code, especially since I am trying to kick this process off as part of my constructor function for the DailyViewModel. I've used a bunch of print comments and it seems like everything is running, but the todaysHabits in my ViewModel is always set to null, and the StreamBuilder doesn't return any items.
Is there anything off in my code that could be causing this?
The DailyViewModel has this todaysHabits property, which is loaded near the bottom of the constructor function:
late Stream<List<HabitCompletionViewModel>> todaysHabits;
DailyViewModel({required WeekDates week}) {
_log.v('initializing the daily viewmodel');
_week = week;
_habitService
.loadActiveHabitsByUser(_loginAndUserService.loggedInUser!.id!)
.then(
(activeUserHabits) {
todaysHabits = _getTodaysHabits(activeUserHabits);
_log.v('todaysHabits has a length of ${todaysHabits.length}');
},
);
setBusy(false);
}
That constructor calls this _getTodaysHabits function which is supposed to convert that Stream<List<Habit>> into a Stream<List<HabitCompletionViewModel>>:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _updateHabitWithNewCompletions(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
}
And my view (which is used the Stacked package to display the contents of the ViewModel and update accordingly) contains this StreamBuilder that should be returning a list of tiles for each HabitCompletionViewModel:
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
Based on pskink's comment, I made the following updates that seem to work. (There is a slight lag when I switch to that view), but it is now showing the correct data.
Basically seems like the issue was that my previous code was returning a list of futures, instead of just a list of HabitCompletionViewModels, and using Future.wait waits for all those to complete.
Pulled out the mapping from List to List into a separate sub-method (here is the main method):
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap(
(habitsList) async => await _mapHabitsToViewModel(habitsList));
}
Updated that sub-method so it first returns a List<Future>, and then uses Future.wait to wait for those to complete as HabitCompletionViewModels before returning that new list:
Future<List<HabitCompletionViewModel>> _mapHabitsToViewModel(
List<Habit> habitsList) async {
List<Future<HabitCompletionViewModel>> newList =
habitsList.map((habit) async {
HabitCompletion completion = habit.completions!.firstWhere((completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
}).toList();
List<HabitCompletionViewModel> transformed = await Future.wait(newList);
return transformed;
}
I want to create a on scroll data load. currently I am getting data(ad list) from a api call as a Future<List<Ads>>
in scroll listener function I just want to append next page data list.
there was a example of doing it with a list, I just want to do the same with a Future list
items.addAll(List.generate(42, (index) => 'Inserted $index'));
You could simply do this:
Future<List<Ads>> appendElements(Future<List<Ads>> listFuture, List<Ads> elementsToAdd) async {
final list = await listFuture;
list.addAll(elementsToAdd);
return list;
}
And then call it like this:
appendedListFuture = appendElements(items, yourItemsToAdd);
So I want to add items to a list of ints, display the sum of that list in another class, and save that sum in SharedPreferences because I want that sum to be displayed (and change based on more items added from another class). I am not sure how to do it. Here's my add function:
class One{
List<int> statTotal = [];
function(){onPressed() => statTotal.add(
int.parse(myInt));}} //this int would always be a different one
And now another class on another page should get that int and add it onto some empty list and display the sum of all of those items:
class Two{
List<int> total = [];
OrderStats({this.total});
//and then display it below in build
build:
Text(widget.totalz.toString())
As I said, I am really not sure how to save the current value of the sum and have it displayed in the second screen until it is changed again when new values are added from the first page. Let me know if I explained it properly, it's a bit messy I know, but I'm not sure how to finish this.
You can use my package rx_shared_preferences.
Screen1:
onPressed() {
statTotal.add(int.parse(myInt));
RxSharedPreferences.getInstance()
.setInt("sum_key", statTotal.fold(0, (acc, e) => acc+ e));
}
Screen2:
StreamBuilder<int>(
stream: RxSharedPreferences.getInstance().getIntStream("sum_key"),
builder: (context, snapshot) {
return Text(snapshot.data?.toString() ?? "Loading...");
}
)
I want to define and invoke a function in Flutter to get required values from Firebase.
In the below code I have defined getCourseDetails() function and invoking it in the container by passing a parameter to get the value.
The Course_Details collection has many documents with course_id's which has attribute with name (course_name, subtitle). I use these values to build a listview cards in next steps.
I am able to get the values from the function using async await, but for some reason the values keeps on updating and never stops. It kind of goes to loop and keeps on running. I added print statements to check and it keeps on running and printing.
Please let me know what wrong I am doing here or how to define function here to avoid the issue. Thanks
class _CourseProgressListState extends State<CourseProgressList> {
String course_name, subtitle;
getCourseDetails(course_id_pass) async {
DocumentSnapshot document = await Firestore.instance.collection('Course_Details').document(course_id_pass).get();
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
}
#override
Widget build(BuildContext context) {
return Container(
child: StreamBuilder(
stream: Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots(),
builder: (context, snapshot) {
if (snapshot.data == null) return Text('no data');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (_, int index) {
var course_details = snapshot.data.documents[index];
getCourseDetails(course_details['course_id']);
Application Flow
On first build of CourseProgressList the Widget State _CourseProgressListState is loaded. This State in the build method uses a StreamBuilder to retrieve all documents from the firestore. As soon as the documents are received the StreamBuilder attempts to build the ListView using the ListViewBuilder.
In the ListViewBuilder you make the async call to getCourseDetails(course_details['course_id']); which when complete populates two attributes String course_name, subtitle;
The problem starts here
Problem
When you call
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
you trigger a Widget rebuild and so the process starts over again to rebuild the entire widget.
NB. refreshing state of a stateful widget will trigger a widget rebuild
NB. Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots() returns a stream of realtime changes implying that your List will also refresh each time there is a change to this collection
Recommendations
If you do not need to call the setState try not to call the setState.
You could let getCourseDetails(course_id_pass) return a Future/Stream with the values desired and use another FutureBuilder/StreamBuilder in your ListViewBuilder to return each ListViewItem. Your user may appreciate seeing some items instead of waiting for all the course details to be available
Abstract your request to firestore in a repository/provider or another function/class which will do the entire workload, i.e retrieving course ids then subsequently the course details and returning a Future<List<Course>> / Stream<List<Course>> for your main StreamBuilder in this widget (see reference snippet below as a guide and requires testing)
Reference Snippet
Your abstraction could look something like this but this decision is up to you. There are many software design patterns to consider or you could just start by getting it working.
//let's say we had a class Course
class Course {
String courseId;
String courseName;
String subTitle;
Course({this.courseId,this.courseName,this.subTitle})
}
Stream<List<Course>> getStudentCourses(int studentId){
return Firestore.instance
.collection('Enrolling_Courses')
.where('student_id', isEqualTo: studentId)
.snapshots()
//extract documents from snapshot
.map(snapshot => snapshot?.data ?? [])
//we will then request details for each document
.map(documents =>
/*because this is an asynchronous request for several
items which we are all interested in at the same time, we can wrap
this in
a Future.wait and retrieve the results of all as a list
*/
Future.wait(
documents.map(document =>
//making a request to firestore for each document
Firestore.instance
.collection('Course_Details')
.document(document['course_id'])
.get()
/* making a final transformation turning
each document into a Course item which we can easily pass to our
ListBuilder/Widgets
*/
.then(courseItem => Course(
courseId:document['course_id'],
courseName:
courseItem.data['course_name'],
subTitle:
courseItem.data['subtitle']
)
)
)
)
);
}
References/Resources
FutureBuilder - https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
StreamBuilder - https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Future.wait - https://api.flutter.dev/flutter/dart-async/Future/wait.html
I have a function working on query an array inside subcollcetion i'ts working good when click it test button its showing me data fine but i need to return the data to the list view How can i do this
function get data
Future < List > getsubcollcation() async {
List Rav;
final firestoreInstance = Firestore.instance;
firestoreInstance.collection("Institute").document(widget.id_document).collection("Ravs").where('Rav name', isEqualTo: 'English').snapshots().listen((snapshot) {
Rav = snapshot.documents.toList();
});
return Rav;
}
So you're not returning a List Widget but a Future Widget. You have to put the Listview into a FutureBuilder widget. Pass the function that returns the Future as it's 'future' parameter. Then write a builder function,
that first checks if the Future has data yet ( if (Future.Connectionstate == Connectionstate.done )),
then you can return a ListView where you put the Future.data as it's 'children' param.