How to use FutureBuilder to increment a variable outside Build method - flutter

I have FutureBuilder that consumes data in my firstore.
The data is of a list of grocery items along with their prices and how many times they were purchased.
The FutureBuilder is showing them in a listview.
What I want to do, is to show the user the total for this particular list.
So I initialized a double before the build method.
While the listview is iterating through the items, I'm adding to the double to get the total and then displaying it at the bottom of the page.
class _PreviousCartState extends State<PreviousCart> {
Future<DocumentSnapshot> snapshot;
#override
void initState() {
super.initState();
snapshot = FirestoreAPI.getWithDate(widget.date);
}
double total = 0;
#override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
child: Column(
children: [
Expanded(
flex: 12,
child: FutureBuilder(
future: snapshot,
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
if (snapshot.hasData) {
if(snapshot.data.data.length == 0) {
return AlertDialog(
content: Text('Wrong Date', textAlign: TextAlign.center, style: TextStyle(color: Colors.black)),
backgroundColor: Colors.redAccent,
actions: [Icon(Icons.keyboard_backspace)],
);
}
List<dynamic> items = snapshot.data.data['items'];
return ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(7),
child: Row(
children: [
Text('Number'),
Text('Name'),
Text('Price')
],
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
),
);
}
index--;
final currentItem = items[index];
this.total += currentItem['price'] * currentItem['count']; // HERE
print('LOOK AT ME $this.total'); // HERE
return Card(
child: ListTile(
leading: Text('${index + 1}'),
title: Center(child: Text(currentItem['name'])),
subtitle: Center(
child: Text(
'''Bought ${currentItem["count"]} times for a total of ${currentItem['price'] * currentItem["count"]} dirhams''')),
trailing: Text('${currentItem["price"]} Dhs'),
),
);
},
);
} else
return Text(snapshot.error.toString());
}
}),
),
Expanded(
flex: 1, child: Center(child: Text('Total: ${this.total}'))),
],
),
);
}
}
After a restart, the total is 0.0.
A hot reload after that, it shows the correct total.
A hot reload after that, it shows double the correct total.
then 3 times the correct total.
and so on ...
This is even though the print statement right after the addition is counting up correctly.
Please explain to me why is this happening??

From what I see is you need to use SetState.
setState(() { total += currentItem['price'] * currentItem['count']; });
According to the docs:
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
When you trigger setState it will rebuild the view and immediately see the changes implied by the new state. Basically it's like hot reload.

Related

Add OnEmpty Widget to ListView.builder() inside FutureBuilder using flutter?

I am using Flutter to develop small application with floor for the database.
I am getting the data from the database using Future then listing all items in UI using FutureBuild.
This is my code
Getting the data from database:
#Query('SELECT * FROM Doctor')
Future<List<Doctor>> findAllDoctor();
Getting data to UI
Future<List<Doctor>> findAllDoctor() async {
return await database.doctorDao.findAllDoctor();
}
Setting data into FutureBuilder:
return FutureBuilder(
future: findAllDoctor(),
builder: (BuildContext context, AsyncSnapshot<List<Doctor>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data?.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
contentPadding: const EdgeInsets.all(8.0),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${snapshot.data![index].firstName} ${snapshot
.data![index].lastName}"),
Text(
snapshot.data![index].phone,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
subtitle: Text(
"${snapshot.data![index].address} ${snapshot.data![index]
.nameOfTheClinic}"),
),
);
},
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
I want to add new widget that tells me no data if there is no data in the table.

Passing data to another screen with Flutter Provider

I'm trying to pass the data to another screen using Provider, but it seems I'm always passing on the same data unless I sort the List and then pass the different data (meaning I'm probably switching the index by sorting the list so that is why it's passing different data now). In short, I call the API, populate the list, setting up the provider too for the next page, and on click I list out the the information from the previous screen, but the problem is I display the same item always unless I sort the list. Here is the code:
Calling the API and displaying the list:
var posts = <RideData>[];
var streamController = StreamController<List<RideData>>();
#override
void initState() {
_getRideStreamList();
super.initState();
}
_getRideStreamList() async {
await Future.delayed(Duration(seconds: 3));
var _vehicleStreamData = await APICalls.instance.getRides();
var provider = Provider.of<RideStore>(context, listen: false);
posts = await _vehicleStreamData
.map<RideData>((e) => RideData.fromJson(e))
.toList();
streamController.add(posts);
provider.setRideList(posts, notify: false);
}
bool isSwitched = true;
void toggleSwitch(bool value) {
if (isSwitched == false) {
posts.sort((k1, k2) => k1.rideId.compareTo(k2.rideId));
} else {
posts.sort((k1, k2) => k2.rideId.compareTo(k1.rideId));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
TextButton(
child: Text('sort ascending'),
onPressed: () {
setState(() {
toggleSwitch(isSwitched = !isSwitched);
});
}),
Container(
height: 1000,
child: StreamBuilder<List<RideData>>(
initialData: posts,
stream: streamController.stream,
builder: (context, snapshot) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Text(
'Ride #${snapshot.data[index].rideId}',
),
),
FlatButton(
textColor: Colors.blue[700],
minWidth: 0,
child: Text('View'),
onPressed: () {
// here is where I pass the data to the RideInfo screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RideInfo(
rideId: snapshot
.data[index].rideId,
)));
},
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'${snapshot.data[index].pickupTime}',
),
Text(
'${snapshot.data[index].jobArrived}',
),
],
),
],
);
},
);
}),
),
],
),
),
),
);
}
After pressing the View button and passing the data to another screen (RideInfo):
class RideInfo extends StatefulWidget {
static const String id = 'ride_info_screen';
String rideId;
RideInfo({#required this.rideId});
#override
_RideInfoState createState() => _RideInfoState();
}
class _RideInfoState extends State<RideInfo> {
String rideID = '';
#override
void initState() {
super.initState();
setState(() {
rideID = widget.rideId;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
'Ride #$rideID',
),
),
body: SafeArea(
child: SingleChildScrollView(
child: Consumer<RideStore>(
builder: (context, rideStore, child) {
return Column(
children: [
ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
RideData rides = rideStore.getRideByIndex(index);
return Column(
children: [
Expanded(
flex: 2,
child: Column(
children: [
Text(
"PICK UP",
),
// here I display the pickUpTime but it is always the same and I wanted to display the time based on the ID
Text(
'${rides.pickupTime}AM',
),
],
),
),
],
);
}),
],
);
},
),
),
),
);
}
}
The data (pickUpTime in this case) doesn't change when I press to see the View of a single item, but like I said, when I change the order of the list with the sort method, then I get the different data.
Here is the Provider model:
class RideStore extends ChangeNotifier {
List<RideData> _rideList = [];
List<RideData> get rideList => _rideList;
setRideList(List<RideData> list, {bool notify = true}) {
_rideList = list;
if (notify) notifyListeners();
}
RideData getRideByIndex(int index) => _rideList[index];
int get rideListLength => _rideList.length;
}
How do I display the correct information based on the ID from the List that I pressed and passed in the Ride Info screen so it doesn't give back always the same data? Thanks in advance for the help!
The offending code is in RideInfo:
ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
RideData rides = rideStore.getRideByIndex(index);
The index is always 1, so you are always showing the first RideData. There are various options to fix it, e.g. pass the index, or even pass the RideData, to the RideInfo constructor:
class RideInfo extends StatefulWidget {
static const String id = 'ride_info_screen';
String rideId;
final int index;
RideInfo({#required this.rideId, #required this.index, Key key})
: super(key: key) {
and:
RideData rides = rideStore.getRideByIndex(widget.index);
I have some additional comments on the code. Firstly, the ListView is serving no purpose in RideInfo, so remove it.
Secondly, there is no need to construct the streamController and to use StreamBuilder in the parent form. Your list is available in the RideStore. So your parent form could have:
Widget build(BuildContext context) {
var data = Provider.of<RideStore>(context).rideList;
...
Container(
height: 1000,
child:
// StreamBuilder<List<RideData>>(
// initialData: posts,
// stream: streamController.stream,
// builder: (context, snapshot) {
// return
ListView.builder(
shrinkWrap: true,
itemCount: data.length,
I hope these comments help.
Edit:
It is simple to edit your code to use FutureBuilder. Firstly, make _getRideStreamList return the data it read:
_getRideStreamList() async {
...
return posts;
}
Remove the call to _getRideStreamList in initState and wrap the ListView in the FutureBuilder that invokes _getRideStreamList:
Container(
height: 1000,
child: FutureBuilder(
future: _getRideStreamList(),
builder: (ctx, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
var data = snapshot.data;
return ListView.builder(
...
);
}
},
),
),
This displays the CircularProgressIndicator while waiting for the data.
Note that this is a quick hack - you do not want to read the data everytime that the widget rebuilds. So _getRideStreamList could check if the data has already been read and just return it rather than rereading.

Flutter: Refreshing ListView.Builder with GetX

I am creating the List of Cards according to the number of toDoId.
toDoController.toDo() is like
toDo = [q1, r4, g4, d4].obs;
And, this is my ListView.builder()
Obx(() {
List _todo = toDoController.toDo();
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: _todo.length,
itemBuilder: (BuildContext context, int i) {
var _loading = true;
var _title = 'loading';
getTodoInfo() async {
_title = await toDoController
.getTodoInfo(
_todo[i]
);
_loading = false;
print(_title); // 'Clean!' <--- returns correct title
}
getTodoInfo();
return Container(
height: 150,
width: 150,
child: _loading
? Text(
_title,
)
: Text(
_title,
),
);
},
);
})
I am trying to make each Container calls the http requests to get the title from my database. Get the title and then update to the Text() widget below. However, it doesn't get updated after the value has been returned from the server.
I could make them wait for the request to get the title by using FutureBuilder. I tried with FutureBuilder too. However, FutureBuilder was not also reactive to the variable changes. So, I am trying to do this here. I kinda get the problem. After, the widget is returned, it is not changeable? Is there any way that I can do it with GetX?
Here's an example of using GetX with a Listview.builder.
This example uses a GetBuilder rather than Obx, as I'm not sure using a stream adds anything of benefit. If for some reason observables/streams are needed, numbers can be updated to be an .obs and the update() calls should be removed and GetBuilder replaced by GetX or Obx. If someone asks, I'll add that as an alternate example.
The GetBuilder wraps the ListView.builder and only the ListView will be rebuilt, not the entire widget tree / page.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ListDataX extends GetxController {
List<int> numbers = List<int>.from([0,1,2,3]);
void httpCall() async {
await Future.delayed(Duration(seconds: 1),
() => numbers.add(numbers.last + 1)
);
update();
}
void reset() {
numbers = numbers.sublist(0, 3);
update();
}
}
class GetXListviewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
ListDataX dx = Get.put(ListDataX());
print('Page ** rebuilt');
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 8,
child: GetBuilder<ListDataX>(
builder: (_dx) => ListView.builder(
itemCount: _dx.numbers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Number: ${_dx.numbers[index]}'),
);
}),
),
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Http Request'),
onPressed: dx.httpCall,
),
RaisedButton(
child: Text('Reset'),
onPressed: dx.reset,
)
],
)
)
],
),
),
);
}
}
Obx / Streams version
Here's the above solution using Rx streams & Obx widget.
class ListDataX2 extends GetxController {
RxList<int> numbers = List<int>.from([0,1,2,3]).obs;
void httpCall() async {
await Future.delayed(Duration(seconds: 1),
() => numbers.add(numbers.last + 1)
);
//update();
}
void reset() {
numbers = numbers.sublist(0, 3);
//update();
}
}
class GetXListviewPage2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
ListDataX2 dx = Get.put(ListDataX2());
print('Page ** rebuilt');
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 8,
child: Obx(
() => ListView.builder(
itemCount: dx.numbers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Number: ${dx.numbers[index]}'),
);
}),
),
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Http Request'),
onPressed: dx.httpCall,
),
RaisedButton(
child: Text('Reset'),
onPressed: dx.reset,
)
],
)
)
],
),
),
);
}
}
I've not tested it due to the fact that I don't have a complete sample but I think this is what you are looking for:
FutureBuilder<String>(
future: toDoController.getTodoInfo(_todo[i]),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Container(
height: 150,
width: 150,
child: Text(snapshot.data),
);
} else if (snapshot.hasError) {
return Text('Error');
} else {
return CircularProgressIndicator();
}
},
),
This is the code you need to return for every item of list builder.

Flutter: Future keeps rebuilding because it is being called inside a stream. How to only make it call again, if possible?

So I wanted to display a list of songs but the future that displays a Uint8List artwork of the songs is called from a future. The code works but the album art looks as if it is glitching because it is constantly being called. I had not idea how to fix this and I have tried many solutions. Please help.
Here is my code:
StreamBuilder<List<SongInfo>>(
stream: widget.songs,
builder: (context, snapshot) {
if (snapshot.hasError)
return Utility.createDefaultInfoWidget(Text("${snapshot.error}"));
if (!snapshot.hasData)
return Utility.createDefaultInfoWidget(
CircularProgressIndicator());
return (snapshot.data.isEmpty)
? NoDataWidget(
title: "There is no Songs",
)
: Column(
children: [
Container(
padding:
EdgeInsets.symmetric(vertical: 10, horizontal: 15),
alignment: Alignment.centerRight,
child: Text("${snapshot.data.length} Songs"),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, songIndex) {
SongInfo song = snapshot.data[songIndex];
return ListItemWidget(
title: Text("${song.title}"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: <Widget>[
Text("Artist: ${song.artist}"),
Text(
"Duration: ${Utility.parseToMinutesSeconds(int.parse(song.duration))}",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500),
),
],
),
trailing: (widget.addToPlaylistAction == true)
? IconButton(
icon: Icon(Icons.playlist_add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(_dialogTitle),
content: FutureBuilder<
List<PlaylistInfo>>(
future: model.getPlayList(),
builder:
(context, snapshot) {
if (snapshot.hasError) {
print("has error");
return Utility
.createDefaultInfoWidget(
Text(
"${snapshot.error}"));
}
if (snapshot.hasData) {
if (snapshot
.data.isEmpty) {
print("is Empty");
return NoDataWidget(
title:
"There is no playlists",
);
}
return PlaylistDialogContent(
options: snapshot.data
.map((playlist) =>
playlist.name)
.toList(),
onSelected: (index) {
snapshot.data[index]
.addSong(
song: song);
Navigator.pop(
context);
},
);
}
print("has no data");
return Utility
.createDefaultInfoWidget(
CircularProgressIndicator());
}),
);
});
},
tooltip: "Add to playlist",
)
: Container(
width: .0,
height: .0,
),
leading: song.albumArtwork == null
? FutureBuilder<Uint8List>(
future: model.audioQuery.getArtwork(
type: ResourceType.SONG,
id: song.id,
size: Size(100, 100)),
builder: (context, snapshot) {
SchedulerBinding.instance
.addPostFrameCallback(
(_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
if (snapshot.data.isEmpty)
return CircleAvatar(
backgroundImage: AssetImage(
"assets/images/title.png"),
);
if (isDataFetched) {
return CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: MemoryImage(
snapshot.data,
),
);
} else {
return CircleAvatar(
child: CircularProgressIndicator(),
);
}
})
: CircleAvatar(
backgroundImage: FileImage(
IO.File(song?.albumArtwork)),
),
);
},
),
),
],
);
},
I would prefer not to set state inside future builders or stream builders using post-frame callbacks. The reason being you basically asking flutter to build the widget again in the next frame while building the current one which recursively sets the whole thing in a loop. Maybe you can create a new stateful widget and do the loading task manually inside the initState if you need those isServiceError and isDataFetched flags.
The problem in your current code seems to be related to:
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Which is called inside the future builder. Everytime you set the state, the same code is called again as the widget is rebuilt thus forming a loop in which the whole thing is built again and again needlessly.
You can avoid it by checking the flags before assigning a post-frame callback like so:
if(!isDataFetched)
{
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isDataFetched = true;
}));
}
So in the next frame, isDataFetched will be true hence no further post-frame callbacks.
This solution however is not really a proper solution because as I mentioned above, it's not a good idea to set the state in future builders using post-frame callbacks. If you don't need those flags outside the future builder, you should simply avoid them and rely on snapshot.hasData and snapshot.hasError inside the builder itself.

Dismissible and FutureBuilder do not work together

It is kind of a complex problem but I'll do my best to explain it.
My project utilizes a sqflite database. This particular page returns a list of Dismissible widgets according to the data in the database. This is how I read the data:
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
Future<List<Task>> allTasks;
#override
void initState() {
allTasks = dbProvider.getAllTasks();
super.initState();
}
void update(){
setState(() {
allTasks = dbProvider.getAllTasks();
});
}
//build
}
The TaskList widget returns a page with a FutureBuilder, which builds a ListView.builder with the data from the database. The ListView builds Dismissible widgets. Dismissing the Dismissible widgets updates a row in the database and reads the data again to update the list.
build method for TaskListState
#override
Widget build(BuildContext context) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets such as a title for the list
),
FutureBuilder(
future: allTasks,
builder: (context, snapshot){
if(snapshot.hasError){
return Text("Data has error.");
} else if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(),
);
} else {
return pendingList(Task.filterByDone(false, Task.filterByDate(Datetime.now, snapshot.data))); //filters the data to match current day
}
},
),
//other widgets
],
);
}
The pendingList
Widget pendingList(List<Task> tasks){
//some code to return a Text widget if "tasks" is empty
return ListView.separated(
separatorBuilder: (context, index){
return Divider(height: 2.0);
},
itemCount: tasks.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index){
return Dismissible(
//dismissible backgrounds, other non-required parameters
key: Key(UniqueKey().toString()),
onDismissed: (direction) async {
Task toRemove = tasks[index]; //save the dismissed task for the upcoming operations
int removeIndex = tasks.indexWhere((task) => task.id == toRemove.id);
tasks.removeAt(removeIndex); //remove the dismissed task
if(direction == DismissDirection.endToStart) {
rateTask(toRemove).then((value) => update()); //rateTask is a function that updates the task, it is removed from the list
}
if(direction == DismissDirection.startToEnd) {
dbProvider.update(/*code to update selected task*/).then((value) => update());
}
},
child: ListTile(
//ListTile details
),
);
},
);
}
Here is the problem (might be a wrong interpretation I'm still kind of new):
Dismissing a widget essentially removes it from the list. After the user dismisses a task, the task is "visually" removed from the list and the update() method is called, which calls setState(). Calling setState() causes the FutureBuilder to build again, but the dbProvider.getAllTasks() call is not completed by the time the FutureBuilder builds again. Therefore, the FutureBuilder passes the old snapshot, which causes the ListView to build again with the Task that just was dismissed. This causes the dismissed ListTile to appear momentarily after being dismissed, which looks creepy and wrong.
I have no idea how to fix this. Any help would be appreciated.
I was having the exact same issue, I was using sqflite which works with Futures so I ended up using the FutureBuilder alongside Dismissible for my ListView. The dismissed list item would remove then reappear for a frame then disappear again. I came across this question :
https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc
which suggests removing the list item from the snapshot data itself:
return FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Dismissible(
onDismissed: (direction) {
snapshot.data.removeAt(i); // to avoid weird future builder issue with dismissible
Provider.of<FoldersProvider>(context, listen: false).deleteFolder(folder.id);
},
background: Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 1,
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(15.0, 0.0, 0.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: UniqueKey(),
direction: DismissDirection.startToEnd,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.folder,
color: Theme.of(context).accentColor,
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
),
),
);
},
);
},
);
and low and behold, it worked! Minimal changes to the code :)
Found a workaround for this by not using FutureBuilder and calling setState after the query is completed.
Instead of Future<List<Task>>, the state now contains a List<Task> which is declared as null.
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
DateTime now = DateTime.now();
List<Task> todayTasks;
//build
}
The update() function was changed as follows
void update() async {
Future<List<Task>> futureTasks = dbProvider.getByDate(now); //first make the query
futureTasks.then((data){
List<Task> tasks = new List<Task>();
for(int i = 0; i < data.length; i++) {
print(data[i].name);
tasks.add(data[i]);
}
setState(() {
todayTasks = tasks; //then setState and rebuild the widget
});
});
}
This way I the widget does not rebuild before the future is completed, which was the problem I had.
I removed the FutureBuilder completely, the Listview.builder just builds accordingly to the List stored in state.
#override
Widget build(BuildContext context) {
if(todayTasks == null) {
update();
return Center(
child: CircularProgressIndicator(),
);
} //make a query if the list has not yet been initialized
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets
pendingList(Task.filterByDone(false, todayTasks)),
],
);
}
This approach completely solved my problem, and I think its better than using a FutureBuilder in case the Future must be completed before the widget builds again.