I have a FutureBuilder here :
FutureBuilder<List<Task>>(
future:
getTasks(),
builder: (BuildContext context, AsyncSnapshot<List<Task>> snapshot){
if(snapshot.hasError){
return const Text('Erreur');
}
if(snapshot.hasData){
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: taskList.map((e) => TaskContainer(
task: e
)).toList(),
);
}
return const Center(child: CircularProgressIndicator());
}
)
Which return a list of tasks as colored block container in a calendar as shown below :
The problem is whenever I navigate to another month in the calendar, the FutureBuilder is fetching the again fetching the data from the webservice, which duplicate the tasks every time I change months :
Here is the code of my functions to navigate between months :
_toPreviousMonth(){
setState(() {
datess.clear();
dayss.clear();
startMonth = new DateTime(startMonth.year, startMonth.month - 1, 1);
});
}
_toNextMonth(){
setState(() {
datess.clear();
dayss.clear();
startMonth = new DateTime(startMonth.year, startMonth.month + 1, 1);
});
}
Actually I don't mind the FutureBuilder fetching the data when I change month, because the final goal would be to display tasks from the selected day.
But what I would like is to have a way to suppress the containers build from the previous fetch as I navigate between months,
Thanks for helping :)
Your code is not showing how you are using snapshot in your FututrBuilder??
I suspect you are adding the tasks directly to a list everytime you re-build using setState( ).
Solution: reset your tasks list at the beginning of the buid( ) method
#override Widget build(BuildContext context) {
yourTaskList = [] //if an array or adjust if other type
Related
I'm having some troubles with the bloc pattern, specifically with BlocConsumer reactions to the emitted states which lead to certain overlaps.
The main character is an object list, for semplicity let's say an object with a String and int parameters.
On the first page I'm adding an event to the bloc to immediately perform the list fetch:
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc()..add(FetchNewList());
}
The layout, a grid view holding buttons with the string param as label follow by the int value, it's build by a BlocConsumer once the list is fetched:
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadingState) {
return CircularProgressIndicator();
} else if (state is ListLoadedState) {
return GridView.count(
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: state.exampleList
.map((e) => FlatButton(
onPressed: () => _openSecondScreen(),
child: Text('${e.label} ${e.value}'),
))
.toList());
} else if (state is ListFetchErrorState) {
return FlatButton(
onPressed: () =>
_exampleBloc = ExampleBloc()..add(FetchNewList()),
child: Text('try again'));
} else {
return Container();
}
},
listener: (context, state) {
//will be used for other purpose
},
),
Non-blocking errors (for problems on list edit operations basically) are shown in a snackbar.
Clicking on a button open a second page, where the same list is loaded as a ListView and some actions allows making changes on list items (for simplicity sake a tap increment the int value of 1 while a long click delete the item from the list):
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadedState) {
return ListView.builder(
itemCount: state.exampleList.length,
itemBuilder: (BuildContext context, int index) {
var item = state.exampleList[index];
return ListTile(
onTap: () => _exampleBloc.add(UpdateItemEvent(
Object(item.label, item.value + 1))),
onLongPress: () =>
_exampleBloc.add(DeleteItemEvent(item)),
title: Text(
'${item.label} + value: ${item.value.toString()}'),
);
});
} else {
return Container();
}
},
listener: (context, state) {
if (state is ListUpdateErrorState) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
} else if (state is ListUpdateInProgressState) {
_showCircularProgressIndicator();
}
},
));
First problem: as the list is the same for both pages, I thought it would be ok to use the same bloc but, even if the last state on the first page is the "ListLoadedState", once the second page is loaded the bloc builder is not building the corresponding widget. As workaround I thought to store the fetched list in a variable in the bloc class and adding an event (GetListAlreadyLoaded) in the initState of the second page to force the bloc to emit once again the ListLoadedState holding the list. Is there a better way to retrieve the last state of the bloc?
Second problem: if any error occurs while performing an update/delete operations on the second page, I would like to simply show a snackbar with an error message. So in the bloc class I have something like this:
#override
Stream<ExampleState> mapEventToState(ExampleEvent event) async* {
//more events...
} else if (event is PerformListUpdate) {
yield ListUpdateInProgressState();
//performing remote update
var updateResult = await _repository.performUpdate(event.objectToUpdate)
//updating local list
if (updateResult.isSuccessful) {
var index = exampleList.indexWhere(
(element) => element.label == event.objectToUpdate.label);
exampleList[index] = event.objectToUpdate;
//triggering the bloc builder with the updated list
yield ListLoadedState(exampleList);
} else {
//emit a state in order to show the error message
yield ListUpdateErrorState(
"An error occurred while performing update");
}
}
}
}
The problem is that if the ListUpdateErrorState is emitted the snackbar is shown, but the bloc builder is triggered by the new state and it rebuilds the widget in the else branch, which is an empty container. As a workaround I thought to first emit the ListUpdateErrorState to allow the listener function react and show the snackbar then, soon after, emit again the ListLoadedState with the last list value in order to trigger also the builder and show again the list view. Is that okay or there's a better way to show errors?
Third problem (basically the same as the second): while performing an asynchronous operation on the second page I would like to show some CircularProgressIndicator without "losing" the list view which could be for example in the appbar, at the bottom of the list or in the middle of the screen above the list. Emitting the "ListUpdateInProgressState" while starting the operation and reacting to it in the bloc listener however triggers the builder function which "destroys" the List view. How can I show the loading indicator without losing the list view?
DBHelper dbHelper = DBHelper();
List<Map<String, dynamic>> lists;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Map<String, dynamic>>>(
future: dbHelper.selectMemo(userkey, 1),
builder: (context, snapshot){
if(snapshot.hasData){
if(snapshot.data.length != 0){
lists = List<Map<String, dynamic>>.from(snapshot.data);
return ListView.separated(
separatorBuilder: (context, index){
return Divider(
thickness: 0,
);
},
itemCount: lists.length,
itemBuilder: (context, index){
return ListTile(
title: Text(lists[index]["memo"]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
setState(() {
lists = List.from(lists)..removeAt(index);
});
},
),
);
},
);
}
}
},
);
}
This is my code. My lists come from sqlflite. And I want to delete my item from Listview. But this code doesn't work. I don't know where I made the mistake.
This behavior is normal. If you print some logs in the build statement, you will find that every time you click the delete button (setState), Widget will build again.
In addition, lists are re-assigned to DB data after each build
lists = List<Map<String, dynamic>>.from(snapshot.data);
So, it looks like the delete operation is not working.
This phenomenon if you've seen Flutter setState part of the source code will be well understood.
In setState, the callback is performed first, and then mark dirty
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
So, there are two ways to solve this problem:
(1) Do not directly change the value of lists, but when the delete button is pressed, to delete the data in the database, so that when Widget build again, the data taken out of the database is correct.
(2) Add a flag to judge whether the data is initialized, and then add a judgment before assigning lists. If the data is initialized, assignment operation will not be carried out
I hope it worked for you. ^-^
I need to display a listview in Flutter with data from firestore. Then I want the user to be able to filter the listview by typing his query in a textfield in the appbar. This is the code I came up with for the listview:
_buildAllAds() {
return StreamBuilder(
stream: Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().map((snap) async {
allAds.clear();
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
}),
builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(child: CircularProgressIndicator());
// }
//else{
//}
if (snapshot.hasError) {
print("err:${snapshot.error}");
}
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
});
});
}
The reason I save the stream data in the List allAds of type ClassificadoData (data items are ads) is because I can then copy it to another List filteredAds on which the user can perform filtering. And the reason I need a stream for allAds is because I want users to be able to see additions/updates in real time.
So this code "works" but it feels a bit awkward and I also can't do nothing with the builder since snaphot remains null all the way (can't show loader during initial data fetch, for example).
Was wondering if there's maybe a more solid way for doing what I want and if it's possible to get a reference to the snapshots down to the builder.
You seem to be mixing two different concepts of using Streams and Stream related Widgets. Ideally you would either use a StreamBuilder and use the data you get from the stream directly on the Widget, or listen to the data and update a variable that is then used to populate your ListView. I've build the latter as an example from your code:
#override
initState(){
_listenToData();
super.initState();
}
_listenToData(){
Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().listen((snap){
allAds.clear();
setState(() {
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
});
});
}
_buildAllAds() {
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
}
);
}
What I'm trying to do
What I'm trying to do is retrieve data from an API call and pass the data in the response to a GridView.count() widget every minute because the data could change.
I did this using a FutureBuilder widget and the Cron functionality from the cron/cron.dart if that helps.
Here is the code:
FutureBuilder<dynamic>(
future: Api.getFoods(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<Widget> slots = [];
if (snapshot.data == null) {
return Text('');
}
var data = snapshot.data;
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
data = snapshot.data;
});
slots = [];
});
for (int i = 0; i < snapshot.data.length; i++) {
slots.add(new FoodSlot(
snapshot.data[i]['name'],
snapshot.data[i]['created_at'],
snapshot.data[i]['qty'].toString()
));
}
return new GridView.count(
crossAxisCount: 2,
scrollDirection: Axis.vertical,
children: slots
);
})
The FoodSlot widget creates a Card and displays the value passed in the arguments on the card.
What I tried
After debugging, I saw that the cron job works fine, but the GridView widgets just won't update.
I tried using a Text widget instead of the GridView and return the values returned by the API call and the widget is updated automatically every 1 minute as expected.
Why is GridView.count() acting like this and how can I fix this?
UPDATE
When the changes in the database are made, the GridView does update, but only when the application is restarted (using R not r).
Solved!
Turns out I had to add the cron job in the FoodSlot widget and update the widget itself periodically.
Ended up adding the following in the initState() function for FoodSlot:
#override
void initState() {
super.initState();
colorSettings = HelperFunctions.setStateColour(expiry);
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
Api.getFood(id).then((res) {
expiry = res['created_at'];
name = res['name'];
qty = res['qty'].toString();
colorSettings = HelperFunctions.setStateColour(expiry);
});
});
});
}
Where id is the id of the database entry referenced by the FoodSlot widget.
Hope this helps someone :)
I am new to Flutter and facing an issue with StreamBuilder & ListView.builder.
I am making a network call on click of a button(Apply Button) available in the list of the card, based on that other buttons are displayed.
the issue I am facing is that widgets are not updated after successful network call but, when I refresh the page I am getting updated result.
What I am doing wrong?
I am not Using any State into this. Do I need to use it?
Widget Code
StreamBuilder(
initialData: [],
stream: dataBoc.ListData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemBuilder: (BuildContext context, index) {
return InkWell(
key: Key(snapshot.data[index]["lid"]),
child: DataCard(
DataModel(snapshot.data[index]),
),
onTap: () {
Navigator.pushNamed(context, "/detailPage",
arguments: snapshot.data[index]["id"]);
},
);
},
itemCount: snapshot.data.length,
);
}
},
),
Bloc Code
//Here is how I am adding data to the stream
if (res.statusCode == 200) {
var data = json.decode(res.body);
if (data['status'] == true) {
// listDataStream.sink.add(data['result']);
listDataStream.add(data['result']);
} else {
listDataStream.sink.addError("Failed to Load");
}
}
Expected result: When I make Network call and if it succeeds then based on network result other buttons must be displayed on the appropriate card.
I have fixed this issue. The issue was my widget tree was not well structured and it was breaking the Widget build process.