Flutter - Pass a Future List to a SearchDelegate - dart

I've been following a Flutter Search tutorial on the - boring flutter show
. I have been trying to implement the same functionality using a list which is derived from a Future List where the data comes from an api (in this case and Aqueduct server).
Currently my screen lists all the contacts from the api, i'd now like to search against that contacts list. I'm assuming it would be best practice to pass the same list (which is already being displayed) to the search delegate. Unfortunately i'm not sure how to achieve this.
My code is as follows (please note i've stripped down some of the code for this examples):
class _ContactsScreenState extends State<ContactsScreen> {
static const _usersUrl = //url info;
static final _token = //token info;
static final _headers = {
"Content-type" : "application/json",
"Accept": "application/json",
"Authorization": "Bearer $_token",
};
HttpRequestStatus httpRequestStatus = HttpRequestStatus.NOT_DONE;
Future<List<Contact>> readContacts() async {
final response = await http.get(_usersUrl, headers: _headers);
List responseJson = json.decode(response.body.toString());
List<Contact> contactList = createContactList(responseJson);
return contactList;
}
List<Contact> createContactList(List data) {
List<Contact> list = List();
for (int i = 0; i < data.length; i++) {
String name = data[i]["name"];
int id = data[i]["id"];
Contact user = Contact(name: name, id: id);
list.add(user);
}
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Contacts'),
actions: [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: (){
showSearch(
context: context,
delegate: ContactSearch(),
);
}
),
],
),
body: Container(
child: FutureBuilder<List<Contact>>(
future: readContacts(),
builder: (context, snapshot) {
if (snapshot.hasData) {
//Code which displays the data (works fine);
}
}
),
)
)
}
}
class ContactSearch extends SearchDelegate<Contact> {
#override
<Widget> buildActions(BuildContext context){
//
}
#override
Widget buildLeading(BuildContext context){
//
}
#override
Widget buildSuggestions(BuildContext context){
//Pass contacts list to here and compares agains 'query'
}
#override
Widget buildResults(BuildContext context) {
return container();
}
}
So in brief, i need to pass the correct list/data through 'ContactSearch()':
showSearch(
context: context,
delegate: ContactSearch(),
);
Thanks in advance.

Basically you have to move the FutureBuilder further up the widget hierarchy, so both the search box as well as the body are below it. Then you can simply push your data into the ContactSearch.
For Example:
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Contact>>(
future: readContacts(),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
title: Text('List Contacts'),
actions: [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: !snapshot.hasData ? null : () {
showSearch(
context: context,
delegate: ContactSearch(snapshot.data),
);
}
),
],
),
body: Container(
child:
(snapshot.hasData ?
//Code which displays the data (works fine);
: /* show errors/progress/etc. */),
),
);
}
);
}

Related

How to display my List<Object> into Listview?

i have an Json File as assets. parsing works perfect, but when i use the list from my json, an error is thrown. "the getter was called on null"
Here my JsonCode:
class Biersorten{
final List<Bier> biersorten;
Biersorten({
this.biersorten
});
factory Biersorten.fromJson(List<dynamic> parsedJson){
List<Bier> listBiersorten = new List<Bier>();
listBiersorten = parsedJson.map((i)=>Bier.fromJson(i)).toList();
return new Biersorten(
biersorten: listBiersorten,
);
}
}
class Bier{
int id;
String name;
String firmenname;
String alkoholgehalt;
Bier({this.id, this.name, this.firmenname, this.alkoholgehalt});
factory Bier.fromJson(Map<String, dynamic> parsedJson){
return Bier(
id: parsedJson["id"],
name : parsedJson["name"],
firmenname : parsedJson ["firmenname"],
alkoholgehalt: parsedJson["alkoholgehalt"]
);
}
}
Here my HomePageCode:
class MyHomePage extends StatelessWidget {
final String title;
Biersorten biersorten;
MyHomePage({this.title}){
loadBier();
}
Future<String> _loadBierAsset() async {
return await rootBundle.loadString("assets/JSON/Beer.json");
}
Future loadBier() async {
String jsonString = await _loadBierAsset();
final jsonResponse = json.decode(jsonString);
biersorten = new Biersorten.fromJson(jsonResponse);
print(biersorten.biersorten[0].alkoholgehalt);
}
Widget getCard(String title){
return Card(
child: Container(
height: 50,
child: Center(
child: Text(title),
),
)
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: <Widget>[
ListView.builder(
itemCount: biersorten.biersorten.length,
itemBuilder: (context, i){
return getCard(biersorten.biersorten[i].name);
},
)
],
),
);
}
}
In the Function _loadBierAsset() i can use the List, but in the ListView.builder it throw an error.
How can i solve this problem?
Thanks for help:)
It is because your function is async and it doesn't return before the list is built. Till that time the list hasn't been initialized.First change your widget from Stateless to Stateful. Then Do this:
body: biersorten!=null ?
Column(
children: <Widget>[
ListView.builder(
itemCount: biersorten.biersorten.length,
itemBuilder: (context, i){
return getCard(biersorten.biersorten[i].name);
},
)
],
): Center(child: CircularProgressIndicator()),
And change your loadBier function to
Future loadBier() async {
String jsonString = await _loadBierAsset();
final jsonResponse = json.decode(jsonString);
setState((){
biersorten = new Biersorten.fromJson(jsonResponse);
});
print(biersorten.biersorten[0].alkoholgehalt);
}
And in your initState() add
loadBier();

Losing data while navigating screens in Flutter

I am new to Flutter and just started to make a tiny little app which takes a list of Top Movies from a server using an async request. and when I tap on top of each one of list items, then it navigates me to another screen to show some details about the movie.
But there is a problem, when I tap on any item to see it's details, inside the details page, when I press back, in the first page, it just loads data again which is not a good user experience. also uses more battery and bandwidth for each request.
I don't know if this is a natural behavior of Flutter to lose data of a Stateful widget after navigating to another screen or there is something wrong with my code.
Can anybody help me with this
This is my code:
import "package:flutter/material.dart";
import "dart:async";
import "dart:convert";
import "package:http/http.dart" as http;
void main() {
runApp(MovieApp());
}
class MovieApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text("Top Movies List",
textDirection: TextDirection.rtl,
style: TextStyle(color: Colors.black87))
]
)
),
body: MoviesList()
)
);
}
}
class MoviesList extends StatefulWidget {
#override
MoviesListState createState() => new MoviesListState();
}
class MoviesListState extends State<MoviesList> {
List moviesList = [];
Future<Map> getData() async {
http.Response response = await http.get(
'http://api.themoviedb.org/3/discover/movie?api_key={api_key}'
);
setState(() {
moviesList = json.decode(response.body)['results'];
});
// return json.decode(response.body);
}
#override
Widget build(BuildContext context) {
getData();
if(moviesList == null) {
return Scaffold(
body: Text('Getting data from server')
);
} else {
return ListView.builder(
itemCount: moviesList.length,
itemBuilder: (context, index){
return Container(
child: ListTile(
title: Text(moviesList[index]['title']),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MovieDetails()),
);
}
)
);
}
);
}
}
}
class MovieDetails extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details')
),
body: Container(
child: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
)
),
);
}
}
Move your getData() method inside the initState() in your State class.
(Remove it from build method)
#override
void initState() {
getData();
super.initState();
}

Flutter: Is it Possbile to have Multiple Futurebuilder or A Futurebuilder for Multiple Future Methods?

So let's say I'm fetching different List from Different Url's
so The Future function will look like this
Future<List<City>> getCityDetails(Region selectedRegion) async {}
Future<List<Region>> getregionDetails() async {}
thus the Widget Builder will look like this
FutureBuilder<List<Region>>(
future: getregionDetails(),
builder: (context, snapshot) {}
is there a way or work around in this one? I'm trying to find an answer how to implement this one or is there another way?
i think you are looking for something like this
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MaterialApp(
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget createRegionsListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty
? Column(
children: <Widget>[
ListTile(
title: Text(values[index].region),
),
Divider(
height: 2.0,
),
],
)
: CircularProgressIndicator();
},
);
}
Widget createCountriesListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
selectedCountry = values[index].code;
});
print(values[index].code);
print(selectedCountry);
},
child: Column(
children: <Widget>[
new ListTile(
title: Text(values[index].name),
selected: values[index].code == selectedCountry,
),
Divider(
height: 2.0,
),
],
),
);
},
);
}
final String API_KEY = "03f6c3123ea549f334f2f67c61980983";
Future<List<Country>> getCountries() async {
final response = await http
.get('http://battuta.medunes.net/api/country/all/?key=$API_KEY');
if (response.statusCode == 200) {
var parsedCountryList = json.decode(response.body);
List<Country> countries = List<Country>();
parsedCountryList.forEach((country) {
countries.add(Country.fromJSON(country));
});
return countries;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load ');
}
}
Future<List<Region>> getRegions(String sl) async {
final response = await http
.get('http://battuta.medunes.net/api/region/$sl/all/?key=$API_KEY');
if (response.statusCode == 200) {
var parsedCountryList = json.decode(response.body);
List<Region> regions = List<Region>();
parsedCountryList.forEach((region) {
regions.add(Region.fromJSON(region));
});
return regions;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load ');
}
}
String selectedCountry = "AF";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Row(children: [
Expanded(
child: FutureBuilder(
future: getCountries(),
initialData: [],
builder: (context, snapshot) {
return createCountriesListView(context, snapshot);
}),
),
Expanded(
child: FutureBuilder(
future: getRegions(selectedCountry),
initialData: [],
builder: (context, snapshot) {
return createRegionsListView(context, snapshot);
}),
),
]),
);
}
}
class Country {
String name;
String code;
Country({this.name, this.code});
factory Country.fromJSON(Map<String, dynamic> json) {
return Country(
name: json['name'],
code: json['code'],
);
}
}
class Region {
String country;
String region;
Region({this.country, this.region});
factory Region.fromJSON(Map<String, dynamic> json) {
return Region(
region: json["region"],
country: json["country"],
);
}
}

Flutter: Firebase basic Query or Basic Search code

The main concept is showing documents or fields which contains the searched alphabet.
The search bar gets the given input, it send to the _firebasesearch(), but in return nothing comes out, and the above image is my database structure, trying to figure out more than a week.
CODE
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_search_bar/flutter_search_bar.dart';
SearchBar searchBar;
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
class DisplayCourse extends StatefulWidget {
#override
_DisplayCourseState createState() => new _DisplayCourseState();
}
AppBar _buildAppBar(BuildContext context) {
return new AppBar(
title: new Text("FIREBASE QUERY"),
centerTitle: true,
actions: <Widget>[
searchBar.getSearchAction(context),
],
);
}
class _DisplayCourseState extends State<DisplayCourse> {
String _queryText;
_DisplayCourseState() {
searchBar = new SearchBar(
onSubmitted: onSubmitted,
inBar: true,
buildDefaultAppBar: _buildAppBar,
setState: setState,
);
}
void onSubmitted(String value) {
setState(() {
_queryText = value;
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You have Searched something!'),
backgroundColor: Colors.yellow,
));
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: searchBar.build(context),
backgroundColor: Colors.red,
body: _fireSearch(_queryText),
);
}
}
Widget _fireSearch(String queryText) {
return new StreamBuilder(
stream: Firestore.instance
.collection('courses')
.where('title', isEqualTo: queryText)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildListItem(snapshot.data.documents[index]),
);
},
);
}
Widget _buildListItem(DocumentSnapshot document) {
return new ListTile(
title: document['title'],
subtitle: document['subtitle'],
);
}
the main concept is showing document sor fields which contains the searched alphabet
the search bar gets the given input, it send to the _firebasesearch(),but in return nothing comes out, and the above image is my database structure, trying to figure out more than a week,
This might sound a ridiculous solution but actually works so well, It's almost like the Like '%' query from SQL
In the TextField as you type a value the inside where() isGreaterThanOrEqualTowill compare it and all the string values greater than the input and If you concatinate a 'Z'
At the end then isLessThan will end just after your search keyword and You get the desired Result from firestore.
// Declare your searchkey and Stream variables first
String searchKey;
Stream streamQuery;
TextField(
onChanged: (value){
setState(() {
searchKey = value;
streamQuery = _firestore.collection('Col-Name')
.where('fieldName', isGreaterThanOrEqualTo: searchKey)
.where('fieldName', isLessThan: searchKey +'z')
.snapshots();
});
}),
I used this Stream in StreamBuilder and It works exactly as expected.
Limitations:
The search is case sensitive(You can convert searchKey to specific case if your data is consistent like Type Case )
You have to start searching from the first letter, it can't search from mid
I'm a bit too late but I just want to share something on how I implement the search function without using third party app in my case. My solution is a bit straight forward querying using firestore. Here's the code:
Future<List<DocumentSnapshot>> getSuggestion(String suggestion) =>
Firestore.instance
.collection('your-collection')
.orderBy('your-document')
.startAt([searchkey])
.endAt([searchkey + '\uf8ff'])
.getDocuments()
.then((snapshot) {
return snapshot.documents;
});
example if you want to search all keywords containing "ab" then it will display all words containing "ab" (ex. abcd, abde, abwe). If you want to make auto suggest search function you can use typehead. which can be found in this link: https://pub.dev/packages/flutter_typeahead
Good luck.
You don't have to rebuild your whole stream, just filter the results from your stream depending on your search string.
Fast, does not need to rebuild the whole stream, finds occurences of the search string not only from the start of a word and is case-insensitive.
return StreamBuilder(
stream: FirebaseFirestore.instance.collection("shops").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) // TODO: show alert
return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return Column(
children: [
Center(
child: CupertinoActivityIndicator()
)
],
);
var len = snapshot.data.docs.length;
if(len == 0)
return Column(
children: [
SizedBox(height: 100),
Center(
child: Text("No shops available", style: TextStyle(fontSize: 20, color: Colors.grey)),
)
],
);
List<Shop> shops = snapshot.data.docs.map((doc) => Shop(
shopID: doc['shopID'],
name: doc['name'],
...
)).toList();
shops = shops.where((s) => s.name.toLowerCase().contains(searchString.text.toLowerCase())).toList();
return
Expanded(
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15),
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: shops.length,
itemBuilder: (context, index) {
return shopRow(shops[index]);
}
),
);
},
);
The issue is you are expecting results from firestore where title is equal to queryText not title contains queryText.
If you want the search feature, you can get and store the firestore documents in a variable something like List<Model> model instead of StreamBuilder and implement search manually from the above stored list of model.
The solution that i found:
List<String> listaProcura = List();
String temp = "";
for(var i=0;i<nomeProduto.length; i++) {
if(nomeProduto[i] == " ") {
temp = "";
} else {
temp = temp + nomeProduto[i];
listaProcura.add(temp);
}
}
The "listaProcura" is the name of the list.
the String "temp" is the name of a temporary string.
This way you will save this list of names in the firebase database.
Will be like:
[0] E
[1] Ex
[2] Exa
[3] Exam
[4] Examp
[5] Exampl
[6] Example
[7] o
[8] on
[9] one
For retrieving this info with the word you wanna search:
await Firestore.instance.collection('name of your collection').where('name of your list saved in the firebase', arrayContains: 'the name you are searching').getDocuments();
This way if you search for "one" and the name is "Example one" the search will return properly.
THIS IS ANOTHER SEARCH CODE THIS WILL SEARCH INSIDE FIREBASE DATABASE
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';
class Db extends StatefulWidget {
#override
HomeState createState() => HomeState();
}
class HomeState extends State<Db> {
List<Item> Remedios = List();
Item item;
DatabaseReference itemRef;
TextEditingController controller = new TextEditingController();
String filter;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
#override
void initState() {
super.initState();
item = Item("", "");
final FirebaseDatabase database = FirebaseDatabase.instance; //Rather then just writing FirebaseDatabase(), get the instance.
itemRef = database.reference().child('Remedios');
itemRef.onChildAdded.listen(_onEntryAdded);
itemRef.onChildChanged.listen(_onEntryChanged);
controller.addListener(() {
setState(() {
filter = controller.text;
});
});
}
_onEntryAdded(Event event) {
setState(() {
Remedios.add(Item.fromSnapshot(event.snapshot));
});
}
_onEntryChanged(Event event) {
var old = Remedios.singleWhere((entry) {
return entry.key == event.snapshot.key;
});
setState(() {
Remedios\[Remedios.indexOf(old)\] = Item.fromSnapshot(event.snapshot);
});
}
void handleSubmit() {
final FormState form = formKey.currentState;
if (form.validate()) {
form.save();
form.reset();
itemRef.push().set(item.toJson());
}
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
centerTitle: true,
backgroundColor: new Color(0xFFE1564B),
),
resizeToAvoidBottomPadding: false,
body: Column(
children: <Widget>\[
new TextField(
decoration: new InputDecoration(
labelText: "Type something"
),
controller: controller,
),
Flexible(
child: FirebaseAnimatedList(
query: itemRef,
itemBuilder: (BuildContext context, DataSnapshot snapshot,
Animation<double> animation, int index) {
return Remedios\[index\].name.contains(filter) || Remedios\[index\].form.contains(filter) ? ListTile(
leading: Icon(Icons.message),
title: Text(Remedios\[index\].name),
subtitle: Text(Remedios\[index\].form),
) : new Container();
},
),
),
\],
),
);
}
}
class Item {
String key;
String form;
String name;
Item(this.form, this.name);
Item.fromSnapshot(DataSnapshot snapshot)
: key = snapshot.key,
form = snapshot.value\["form"\],
name = snapshot.value\["name"\];
toJson() {
return {
"form": form,
"name": name,
};
}
}
if Search list is case senstive like this :
Curaprox Be You Display
Curaprox Black is White Display
Curaprox Black is White Mini Display
Curaprox Hydrosonic Pro Display
Curaprox Large Interdental Brush Display
then :
response = await FirebaseFirestore.instance
.collection('pointOFSale')
.orderBy("title")
.startAt([val.capitalize()]).endAt(
[val[0].toUpperCase() + '\uf8ff']).get();
Extension code :
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
if List is like :
curaprox be you display
curaprox black is white display
curaprox black is white mini display
then :
response = await FirebaseFirestore.instance
.collection('pointOFSale')
.orderBy("title")
.startAt([val]).endAt([val + '\uf8ff']).get();
soo simple and fast.
if (text.length > 1) {
setState(() {
tempSearchStore = _listPkh.documents.where((d) {
if (d['nama'].toLowerCase().indexOf(text) > -1) {
return true;
} else if (d['alamat'].toLowerCase().indexOf(text) > -1) {
return true;
}
return false;
}).toList();
});
} else {
setState(() {
tempSearchStore = _listPkh.documents;
});
}

screen not re-rendering after changing state

I'm just starting with Flutter, finished the first codelab and tried to add some simple functionality to it.
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData(primaryColor: Colors.deepOrange),
home: new RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
#override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = new TextStyle(fontSize: 18.0);
final _saved = new Set<WordPair>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.list),
onPressed: _pushSaved,
)
],
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
},
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new IconButton(
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
onPressed: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
));
}
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return _buildRow(pair);
// new ListTile(
// title: new Text(
// pair.asPascalCase,
// style: _biggerFont,
// ),
// );
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
}
In the Save suggestions screen I built the same row as in the Sugestions Screen.
In the Saved Sugstions screen when you click the heart icon the element is removed from the array of saved items but the screen is not re-rendered.
what am I doing wrong here?
thanks!
Update
Actually your app is working perfectly fine with me :/
Because you are not communicating the state change with the icon change. You are already changing state based on alreadySaved, notice how you managed setState()
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
In the previous block you are only removing or adding to your favourite list based on the boolean value of alreadySaved and you are not telling setState to change anything else. That is why the following does not produce a re-render even though alreadySaved is switching values
///These two lines do not know what is happening
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
So you can instead do the following
icon: new Icon(_whichIcon), //initialized var _whichIcon = Icons.favorite_border
color: _whichIconColor, //Initialized var _whichIconColor = Colors.transparent
And your setState would be:
setState(() {
if (alreadySaved) {
_saved.remove(pair);
_whichIcon = Icons.favorite_border ;
_whichIconColor = Colors.transparent;
} else {
_saved.add(pair);
_whichIcon = Icons.favorite ;
_whichIconColor = Colors.red;
}
});
Or simpler you can do it like this, and keep your icon logic unchanged:
bool alreadySaved = false;
...
setState(() {
if (_saved.contains(pair)) {
_saved.remove(pair);
alreadySaved = false;
} else {
_saved.add(pair);
alreadySaved = true;
}
});

Resources