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.
Related
I am building an application in flutter which include a feed of posts, where each post is a card, and each post can be liked by user. The build of the screen depends on future builder that get the collections from posts from Firestore, but something weird is happening : On the first build of the screen, the variable "isliked" (by specific user) of a post is not updated, even all the variables are loaded as it has to be, causing the like icone to be wrong (filled when liked, empty else). After scrolling through the posts, the variable is updated and also the like icon.
The code of the feed :
Widget build(BuildContext context) {
return FutureBuilder(
future: context.watch<Storage>().getAllPosts(context.read<AuthNotifier>().user!.uid),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Text(snapshot.error.toString(),
textDirection: TextDirection.ltr)
)
);
}
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: snapshot.data?.length,
itemBuilder: (BuildContext context, int index) {
for (var element in snapshot.data!) {
print('hello - ${element.is_liked}');
}
return Padding (
padding: const EdgeInsets.all(8.0),
child: PostWidget(
snapshot: snapshot,
index: index,
isliked : snapshot.data![index].is_liked)
);
},
),
)
);
}
And the post widget:
Widget build(BuildContext context) {
print('helllooooo ${widget.isliked}');
print(widget.snapshot.data?[widget.index].is_liked);
return Card(
elevation: 4,
shadowColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
[Some text widgets]
Row(
children: [
IconButton (
onPressed: () async {
if(widget.isliked){
await context.read<Storage>().removeLike(widget.snapshot.data![widget.index].id, context.read<AuthNotifier>().user!.uid);
setState(() {
widget.isliked = false;
widget.snapshot.data![widget.index].likes --;
});
}
else {
await context.read<Storage>().addLike(widget.snapshot.data![widget.index].id, context.read<AuthNotifier>().user!.uid);
setState(() {
widget.isliked = true;
widget.snapshot.data![widget.index].likes ++;
});
}
},
icon: Icon (
widget.isliked ? Icons.thumb_up : Icons.thumb_up_outlined,
color: Colors.blue,)),
Text(widget.snapshot.data![widget.index].likes.toString()),
When I try to put the post widget into the feed, the problem disappear, but for all press on like button I have to rebuild the entire feed screen, which is not appreciable by user, and also problematic because the transaction that update the like can take more time than rebuild the screen.
I have local json file in my flutter project and I'm able to display it on the initial load of the screen but when I try to implement search bar, the data is not displaying anymore for some reason.
I have been following YouTube tutorial but I couldn't find any video that filter local json file so I would be really appreciated if anyone can look at my code below and tell me where I did wrong.
I would be really appreciated if I can get any help or suggestion on why my data is not loading/displaying on the screen anymore.
import 'dart:convert';
import 'package:falamhymns/models/sawnawk_model.dart';
import 'package:falamhymns/sub_screens/detail_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' as rootBundle;
class SawnawkScreen extends StatefulWidget {
SawnawkScreen({Key? key}) : super(key: key);
#override
State<SawnawkScreen> createState() => _SawnawkScreenState();
}
class _SawnawkScreenState extends State<SawnawkScreen> {
TextEditingController controller = new TextEditingController();
List<SawnAwkModel> _searchResult = [];
List<SawnAwkModel> _userDetails = [];
#override
void initState() {
super.initState();
ReadJsonData();
}
onSearchTextChanged(String text) async {
_searchResult.clear();
if (text.isEmpty) {
setState(() {});
return;
}
_userDetails.forEach((userDetails) {
if (userDetails.titleFalam.contains(text) ||
userDetails.titleEnglish.contains(text))
_searchResult.add(userDetails);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: ReadJsonData(),
builder: (context, data) {
return new Column(
children: <Widget>[
new Container(
color: Colors.blue,
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new Card(
child: new ListTile(
leading: new Icon(Icons.search),
title: new TextField(
controller: controller,
decoration: new InputDecoration(
hintText: 'Search', border: InputBorder.none),
onChanged: onSearchTextChanged,
),
trailing: new IconButton(
icon: new Icon(Icons.cancel),
onPressed: () {
controller.clear();
onSearchTextChanged('');
},
),
))),
),
new Expanded(
child: _searchResult.length != 0 ||
controller.text.isNotEmpty
? ListView.builder(itemBuilder: (context, index) {
return Card(
elevation: 5,
margin: EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
child: new InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(
_userDetails[index].pageNumber),
));
},
child: Container(
padding: EdgeInsets.only(bottom: 8),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(8),
child: Text(_userDetails[index]
.titleFalam
.toString()),
),
Padding(
padding: EdgeInsets.all(8),
child: Text(_userDetails[index]
.titleEnglish
.toString()),
),
],
),
),
),
);
})
: new ListView.builder(
itemCount: _userDetails.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
title: new Text(
_userDetails[index].titleFalam)),
);
}))
],
);
}));
}
}
Future<List<SawnAwkModel>> ReadJsonData() async {
final jsondata =
await rootBundle.rootBundle.loadString('data/sawnawk_data.json');
final list = json.decode(jsondata) as List<dynamic>;
return list.map((e) => SawnAwkModel.fromJson(e)).toList();
}
A couple problems with your code.
Don't call async functions in initstate method, use FutureBuilder only for that. In your code you're calling it in both places, doesn't make sense.
You fetched the data in the FutureBuilder call, but didn't used it anywhere in your code. _userDetails was initialized to empty array and never given the fetched data. The code is displaying this empty array.
You're displaying not the _searchResults, rather _userDetails. So search won't change anything. And to re-render the widget with the filtered data, you need to call the setState method.
Even if you solve the above mentioned problems and the data appears on your screen, what will happen is that for each key you'd press in the search box, it will rebuild the widget, refetch the data, again reading the json file. This is because the stateful widget responsible for handling search changes its state on each keystroke to re-render, rebuilding the FutureBuilder along with it.
You need to completely reorganize the widgets. Have a stateless widget return FutureBuilder that fetches the data, and returns a stateful widget that takes in the data to display it and has that search bar.
Something like this:
class SawnawkScreen extends StatelessWidget {
const SawnawkScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: ReadJsonData(),
builder: ((BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState != ConnectionState.waiting) {
return SawnawkBuild(_userDetails : snapshot.data ?? []);
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
return const Center(child: CircularProgressIndicator());
}
}),
);
}
}
Here SawnAwkBuild widget displays the list of data and contains the search box. Initialize _userDetails with the received data in its initState function, like this
#override
initState() {
_userDetails.addAll(widget._userDetails);
_searchResults = _userDetails;
super.initState();
}
And then display values from _searchResults in this widget, and in the search filter function use setState to change it.
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.
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.
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.