Flutter Like button functionality using Futures - dart

I'm trying to build a Save button that lets the user save/ unsave (like/ unlike) items displayed in a ListView.
What I have so far:
Repository that provides a Future<bool> that determines which state the icon should be rendered in
FutureBuilder that calls the repository and renders the icon as either saved/ unsaved.
Icon wrapped in a GestureDetector that makes a call to the repository within a setState call when onTap is invoked.
`
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _repository.isSaved(item),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.none:
case ConnectionState.active:
return Icon(Icons.favorite_border);
case ConnectionState.done:
return GestureDetector(
child: Icon(
snapshot.data ? Icons.favorite : Icons.favorite_border,
color: snapshot.data ? Colors.red : null),
onTap: () {
setState(() {
if (snapshot.data) {
_repository.removeItem(item);
} else {
_repository.saveItem(item);
}
});
},
);
}
});
}
`
The issue I'm having is that when I tap to save an item in the list - the item is saved however the icon is not updated until I scroll it off screen then back on again.
When I tap to unsave an item, it's state is reflected immediately and updates as expected.
I suspect that the save call is taking longer to complete than the delete call. Both of these are async operations:
void removeItem(String item) async {
_databaseClient.deleteItem(item);
}
void saveItem(String item) async {
_databaseClient.saveItem(item);
}
#override
void deleteItem(String item) async {
var client = await db;
client.delete("items_table", where: "item = '$item'"); // returns Future<int> but I'm not using this currently
}
void _saveItem(String item) async {
var client = await db;
client.insert("items_table", item); // returns Future<int> but I'm not using this currently
}
Future<bool> isSaved(String name) async {
var matching = await _databaseClient.getNameByName(name);
return matching != null && matching.isNotEmpty;
}
Any idea what could be causing this?

When you tap the button, setState will be called. then FutureBuilder will wait for the isSaved method. if the save method is being in progress. isSaved will return the last state and Icon will not change.
I suggest to wait for the result of Save and Remove method and call setState after that.
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _repository.isSaved(item),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.none:
case ConnectionState.active:
return Icon(Icons.favorite_border);
case ConnectionState.done:
return GestureDetector(
child: Icon(
snapshot.data ? Icons.favorite : Icons.favorite_border,
color: snapshot.data ? Colors.red : null),
onTap: () async{
if (snapshot.data) {
await _repository.removeItem(item);
} else {
await _repository.saveItem(item);
}
setState(() {
});
},
);
}
});
}
However, if the methods take so long, it delays which cause bad user experience. it better to change the icon to progress circle during running methods.

Related

ListView isnt updating state after I added a FutureBuilder

Before this problem happen I was dealing with Future handling to return the values I saved on a sharedPreference, however my _deleteTodo method was working just fine.
After I added the FutureBuilder and finally got my values rendered on the UI now I'm struggling with this new bug.
Every time I update the state of and item my UI reflects it but immediately undoes it
I tried to see if it was something with my _deleteTodo method so I changed the setState to only change the boolean from false to true, but it does exactly the same.
I also print the length of my List after the _deleteTodo and something funny happens: it works one time, the _deleteTodo erase the Todo but after that it doesn't work anymore
This is my TODO class
class Todo {
Todo ({this.title,this.isDone = false});
String title;
bool isDone;
//Decode method to convert a Json String into a Dynamic object
Todo.fromJson(Map <String, dynamic> json)
: title = json ["title"],
isDone = json ["isDone"];
Map <String,dynamic> toJson() =>
{
"title" : title,
"isDone" : isDone
};
}
This is my screen
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [];
//updates the state of the checkbox and reflects it on the UI
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
_deleteTodo(todo,isChecked);
print(todos.length);
});
}
_addTodo() async {
final todo = await showDialog<Todo>(
context: context,
builder: (BuildContext context) { // <- Here you draw the
Dialog
return NewTodoDialog();
},
);
if (todo != null) {
setState(() {
todos.add(todo);
_saveTodo(todos);
print(todos.length);
});
}
}
_deleteTodo (Todo todo, bool isDone) => (isDone)?
todos.remove(todo): debugPrint;
//Save you array object as an array of Strings in Shared Preferences
Future<void> _saveTodo(List<Todo> todo) async {
SharedPreferences sharedPreferences = await
SharedPreferences.getInstance();
sharedPreferences.setStringList("savedData", _mapTodoData(todo));
}
_mapTodoData(List<dynamic> todos) {
try {
var res = todos.map((v) => json.encode(v)).toList();
return res;
} catch (err) {
// Just in case
return ["Nope"];
}
}
Future<List> loadData() async {
SharedPreferences sharedPreferences = await
SharedPreferences.getInstance();
final List<Todo> todoArray =
_decodeTodoData(sharedPreferences.
getStringList("savedData")).toList();
todos = todoArray;
return todoArray;
}
List<Todo> _decodeTodoData(List<String> todos) {
try {
//Transforming List<String> to Json
var result = todos.map((v) => json.decode(v)).toList();
//Transforming the Json into Array<Todo>
var todObjects = result.map((v) => Todo.fromJson(v)).toList();
return todObjects;
} catch (error) {
return [];
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Colors.deepPurple[900],
title: Text('Todo List')),
body: Container(
child: FutureBuilder(
future: loadData(),
builder: (BuildContext context, AsyncSnapshot snapshot){
return TodoList(
todos: todos,
onTodoToggle: _toggleTodo,
);
})
)
,
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.purpleAccent[700],
child: Icon(Icons.add),
onPressed: _addTodo,
),
);
}
}
Thanks in advance, hope someone can help me :)
From what I can tell is that your _deleteTodo deletes from the local instance list todos but you are instructing flutter to rebuild the UI from disk via loadData which re-gets the data from disk.
My suggestion is to get the data once on page load using initState and from there only refer to the local instance of todos.
In your _deleteTodo you would also need to persist the state to disk or have a commit button somewhere
I Found the solution, inside my _toggleTodo i need it to save my TodoList after doing something to my todo.

snapshot.ConnectionState always waiting

In a FutureBuilder, snapshot.ConnectionState is always waiting, but the future function completed successfully. While in a different page the same block of code works and the ConnectionState transitions from waiting to done.
The future function:
Future getDoctors() async {
var res = await http.get(globals.domain + "users/docs/");
var resBody = json.decode(utf8.decode(res.bodyBytes));
print(res.statusCode);
if (res.statusCode == 200) {
if (mounted) {
setState(() {
doctors = resBody;
});
}
}
}
The future builder:
FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, snapshot) {
print(snapshot);
}
)
Actual result:
AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null)
Expected result: transiton to AsyncSnapshot<dynamic>(ConnectionState.done, null, null)
EDIT1:
while debugging noticed that the future function getDoctors() is called periodically, I guess that's why the snapshot is always waiting
Calling setState causes the widget tree to be rebuild. Therefore, when the FutureBuilder is rebuilt, the getDoctors() function is invoked again, causing an infinite loop (getDoctors()->setState()->rebuild->getDoctors()...)
The solution is to either remove setState from the getDoctors method or invoke getDoctors() once in the initState() method, store the Future and pass it to the FutureBuilder, thus ensuring that it is only done once.
Future _doctorsFuture;
initState() {
...
_doctorsFuture = getDoctors();
}
.
.
// Somewhere in build()
FutureBuilder(
future: doctorsFuture,
builder: (BuildContext context, snapshot) {
print(snapshot);
}
),
You need to return the value from the Future to finish the connection. Change your Future to something like this:
Future<dynamic> getDoctors() async {
var res = await http.get(globals.domain + "users/docs/");
if (res.statusCode == 200) {
return json.decode(utf8.decode(res.bodyBytes));
}
return null;
}
And change your FutureBuilder to this:
FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
print(snapshot);
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData && snapshot.data != null) {
return Center(...);
}
return Center(child: CircularProgressIndicator());
},
)
Please modify to match your variables and classes.
Change the return of the Future like:
`Future<String> getDoctors() async{
var res = await http.get(globals.domain + 'users/docs/');
var resBody = json.decode(utf8.decode(res.bodyBytes));
if (res.statusCode == 200) {
if (mounted) {
setState(() {
doctors = resBody;
});
}
}
}`
and the FutureBuilder:
`FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
break;
case ConnectionState.waiting:
return CircularProgressIndicator(
color: Colors.black54,
strokeWidth: 2,
);
case ConnectionState.active:
break;
case ConnectionState.done:
return Container(/*here write your return code*/);
}
return Text('Some error ocurred try getDoctor');
},
);`

for loop inside the firebase listen is not updating

In my application i want call data from firebase different collections. First I want to list all items and take the id.
Using that id i want to retrieve price from price collection. After that i want to retrieve data from discount. for taking discount.
Here i am using loops.
In the below code the output is coming. First loading list after that it calling second collection price.
Any one know the solution.
I want to listen for calling three collection. Because if any data change i want to update.
#override
void initState() {
super.initState();
_loadItems();
}
Future _loadItems() async {
int price;
int discount;
//calling first collection for getting id and name
firestore.collection("item").snapshots().listen((itemData)async{
for(int i=0;i<itemData.documents.length;i++){
// calling second collection for getting price
firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
price=priceData.documents[0].data['price'];
debugPrint("price showing before loading:"+price.toString());
//calling third collection for getting discount
firestore.collection("discount")
.where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((discountData) async{
for(int j=0;j<discountData.documents.length;j++){
discount=discountData.documents.data['discount'];
}
});
});
setState(() {
debugPrint("price showing after loading:"+price.toString());
this.documents.add(new CartProduct(
name:itemData.documents[i].data["id"],
label:itemData.documents[i].data["label"],
price:price,
discount:discount
));
});
}
});
}
Present output
price showing after loading:0
price showing after loading:0
price showing after loading:0
price showing before loading:10.0
price showing before loading:10.0
price showing before loading:10.0
Expected output
price showing before loading:10.0
price showing before loading:10.0
price showing before loading:10.0
price showing after loading:10.0
price showing after loading:10.0
price showing after loading:10.0
I thing you can use nested StreamBuilder's
Widget getTripleCollectionFromFirebase() {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("item").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// Do your staff after first query then call the other collection
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection("price")
.where("id", isEqualTo: "fill_it_with_your_code")
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// do your staff after second Query
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection("discount")
.where("id", isEqualTo: "something")
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// do your staff after third Query
// return the widget which you want to build when all data comes.
}
}
},
);
}
}
},
);
}
}
},
);
}
This is my code. I will explain it step by step so you can convert it to your's.
buildUserActions returns a StreamBuilder that StreamBuilder takes all documents which is in actions collection in cloud firestore. When ConnectionState is active, or done if I have data I assign it to variable named _lastActionDocuments.
QuerySnapshot _lastActionDocuments;
Stream<String> streamOfFillActionFields;
Widget buildUserActions() {
return StreamBuilder(
initialData: _lastActionDocuments,
stream: Firestore.instance.collection('actions').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
if (!snapshot.hasData) return Text('No data finded!');
_lastActionDocuments = snapshot.data;
streamOfFillActionFields = fillActionFields();
return reallyBuildActions();
}
},
);
}
then I have a Stream function
Stream<String> fillActionFields() async* {
try {
List<ActionModel> newActionList = [];
for (DocumentSnapshot actionSnapshot in _lastActionDocuments.documents) {
var currentAction = ActionModel.fromSnapshot(actionSnapshot);
// I awaiting to get and fill all data.
await currentAction.fillAllFields();
newActionList.add(currentAction);
}
actionList = newActionList;
// what I yield is not important this case
yield 'data';
} catch (e) {
print(e);
yield 'nodata';
}
}
currentAction.fillAllFields basicly that function ask to firebase to get the related data to fill all fields in my Action Object.
Future<void> fillAllFields() async {
DocumentSnapshot ownerSnapshot = await ownerRef.get();
owner = UserModel.fromSnapshot(ownerSnapshot);
DocumentSnapshot routeSnapshot = await routeRef.get();
route = RouteModel.fromSnapshot(routeSnapshot);
}
then I have another widget which is returning a StreamBuilder. this widget build the real UI widget(buildAllActions) after all data arrived from reference calls.
Widget reallyBuildActions() {
return StreamBuilder(
stream: streamOfFillActionFields,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == 'data') {
return buildAllActions();
} else {
return Center(
child: Column(
children: <Widget>[
CircularProgressIndicator(),
Text('Data Loading...')
],
),
);
}
}
},
);
}
I have got answer Use StreamSubscription and call one by one. First I run one loop and check whether it is completed or not than after only call second loop. It working fine but taking delays. when I using StreamBuilder it not completing the request. I don't know why it happening. My code is shown below.
StreamSubscription<QuerySnapshot> streamSub1;
StreamSubscription<QuerySnapshot> streamSub2;
StreamSubscription<QuerySnapshot> streamSub3;
var list = new List();
_loadItems() {
int price;
int discount;
int count =1;
//calling first collection for getting id and name
streamSub1= firestore.collection("item").snapshots().listen((itemData)async{
for(int i=0;i<itemData.documents.length;i++){
list.add(id:itemData.documents[0].data['id'],name:itemData.documents[0].data['id');
if(onFavData.documents.length==productCount){
debugPrint("loop completed");
_loadPrice();
}
}
});
}
void _loadPrice(){
streamSub1.cancel();
int count =1;
for(int i=0;i<list.length;i++){
streamSub2= firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
list[i].price= priceData['price'];
if(count==list.length){
debugPrint("loop completed");
_loadDiscount();
}
});
}
}
_loadDiscount();{
streamSub2.cancel();
int count =1;
for(int i=0;i<list.length;i++){
streamSub3= firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
list[i].discount= priceData['price'];
if(count==list.length){
debugPrint("loop completed");
}
});
}
}

How to navigate different page after async method return response

I have a screen App in which i have onGenerateRoute property of MaterialApp. In the routes method i make an api call and once i get the response i want to let user navigate to login screen
I tried calling my widget Login inside .then() function
class App extends StatelessWidget {
Widget build(BuildContext context) {
return AppBlocProvider(
child: LoginBlocProvider(
child: MaterialApp(
onGenerateRoute: routes,
),
),
);
}
Route routes(RouteSettings settings) {
print(settings.name);
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) {
//HERE I AM MAKING API CALL
final appBloc = AppBlocProvider.of(context);
appBloc.verifyUser().then((response) {
//HERE ONCE I GET THE RESPONSE I WANT TO NAVIGATE USER TO
//lOGIN ACTIVITY
print('called');
return Login();
});
return AppBlocProvider(
child: Center(child: CircularProgressIndicator()),
);
});
break;
case '/Login':
return MaterialPageRoute(builder: (BuildContext context) {
return Login();
});
break;
case '/HomeScreen':
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
break;
}
return MaterialPageRoute(builder: (context) {
print('returned null');
});
}
api call get successful and even .then() method executes but login screen doesn't appear
The reason return Login(); doesn't do anything was because another return has been executed already: return AppBlocProvider(child: Widget());
Similar to this sample, since a return has been already made, the other return won't do anything. The sample prints 'bar', and 'foo' was never printed using print(bar());.
void main() {
print(bar());
}
Future<String> foo() async{
await Future.delayed(Duration(seconds: 5));
return 'foo';
}
String bar(){
String txt = 'bar';
foo().then((String value){
print('Future finished: $value');
// Since print already got a String return,
// returning this value won't do anything
return value; // 'foo' won't be printed on main()
});
return txt;
}
You may want to consider moving the navigation inside Login and also display CircularProgressIndicator() there.

Usage of FutureBuilder with setState

How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}
Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}
Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}

Resources