Flutter identify Widget built by loop or listview.builder - flutter

I can't get around this issue, how do I select Widget built by for loop or a ListView.builder?
In code I will post, I have buttons built by for loop,
I want to change button's background color on click.
I'm using Provider in my project so this function is implemented with Provider.
In my Provider class I made a boolean and a function to switch that boolean, and then I assigned it to buttons, and set it's color to value of that boolean with ternary operator.
While this somewhat works, the problem is... All of the buttons built by the for loop are affected,
meaning. I press one button, all of them change color.
How do I fix this?
My widget which includes mentioned buttons:
ListView(
children: [
for(int i = 0; i < recipeIngredients.length; i++)
IngredientsTab(
ingredientText: recipeIngredients[i],
textColor: categoryAndSeeAllColor,
width: width,
**buttonColor: provider.isButtonPressed ? accentColor : Colors.grey[100],**
buttonIcon: Icons.add,
buttonText: 'Add to cart',
buttonTextColor: accentColor,
**buttonAction: provider.changeBackground,**
),
],
),
In my Provider class:
bool isButtonPressed = false;
void changeBackground() {
isButtonPressed = isButtonPressed ? false : true;
notifyListeners();
}
}
While I understand this happens for obvious reason, I'm assigning this isButtonPressed to all buttons so when its true they all change... So that makes sense, I just don't know how to solve it, I'm also having similar issue in another project where items are built with ListView.builder...
So if someone would help me with this would mean a lot to me :)

Usually you have in the Provider a List of Ingredients.
Each Ingredient have an ID and a bool selected variable.
The buttonAction calls the method with the ID:
void changeBackground(int ingredientID) {
ListOfIngredients.singleWhere((elem)=>elem.ID==ingredientID)).isButtonPressed = !isButtonPressed;
notifyListeners();
}
Then you build the ListView using the List of Ingredients and for each Ingredient you check the isButtonPressed value.

Related

Flutter: setState not updating screen in Async function

From initState() I call setDefaults(), which builds a Person object, having done this execution should return to initState which calls getPrefs(), an async function.
#override
void initState() {
super.initState();
setDefaults();
getPrefs();
}
getPrefs calls SharedPreferences.instance() and retrieves some data from prefs - hooyah!
This is compared with some fields of the Person object, finally setState() is called to update the screen with a Tile colour, and to render a Banner widget if (usersTile == true).
This never happens, setState never updates the screen. I don't know why.
Future<void> getPrefs() async {
prefs = await SharedPreferences.getInstance();
List<String> neighbourProfilesDownloaded = prefs!.getStringList("downloadedPersonProfiles") ?? [];
neighbourProfilesDownloaded.add(widget._person!.firebaseId!);
prefs!.setStringList("downloadedPersonProfiles", neighbourProfilesDownloaded);
usersTile = widget._person!.firebaseId == prefs!.getString("firebaseId") ?? "";
setState(() {
tileColor = usersTile ? Colors.grey.shade900 : Color(0xFF462c22);
usersTile;
});
}
This question has been asked before. I've tried looking at other solutions on StackOverflow, so far as I can see I'm already following their proposed solutions, to await the asynchronous call then setState after it. And none of the proposed answer have been marked correct anyway.
I've tried assigning the value of usersTile inside setState, no difference.
The Tile colour does sometimes work, however not on screen load. If I scroll down the Tile list, swiping the usersTile out of the viewport, then swiping back up to return to it, I find that DOES update the Tile's colour and Banner. Indicating the Tile property assignments are working but setState is not.
This is not satisfactory, it screams buggy to the user.
I know this can work, because I have another class, another ListView screen in my app which seems to reliably update the screen on load. I've studied the control flow on that screen and can't see how it's different. Quite the boggle.
This is the Card Widget, with properties I want to adjust, from build()
return Card(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0))),
color: tileColor,
key: ValueKey(widget._person!.firebaseId! + "LoaningTileCard"),
child: usersTile ? Banner(
message: "YOURS",
location: BannerLocation.topEnd,
color: Colors.red,
child: CardContentWidget)
: CardContentWidget);
I think CardContentWidget uses Card in build method, Card has default background color as Colors.white by using CardTheme.of(context).
You may have to reset with transparent or user Container instead.

How do i modify the data of an existing variable in flutter?

I want to make an editable TextWidget in flutter but I don't really know how to go around it, I did some research, but still can't find a good solution.
Here's my sample code below.
I have a variable called
int qty = 1;
and so I called the variable in TextWidget
Column(
children: [
Text(
"${qty}",
style: TextStyle(),
)
],
),
I want to have these features that make user tab on the value to change it if they want, upon tap, a pop-up dialog will show to give the user the ability to change the existing value to whatever the user wants.
Please if anyone knows how, please help.
You will need a statfull widget to call setState and make the UI update with the new value stored in your qty variable. (I'am assuming that you are not using any state managment).
I wrote a possible solution for what you need.
Let look into some considerations:
Text will show whatever is in the qty as long we call setState after (or do it inside) we change the value of qty.
You need some widget to detect your tap. If you want to the text be 'clicable' then it should be wraped inside that widget.
The onTap/onPress call back of that widget should show a new widget. For this you can use the already made showDialog() and pass it a Dialog Widget. in here you will put your ui for that.
In some point of that UI you need to introduce the new value. So you can use a simple TextField that will save the introduced value, where you can assign it to qty, without forgetting to call setState! Note that it deal with strings, so you neet to do an int.parse() ou double.parse accordingly to you qty var type.
And I think that's it.
The could be other ways of doing it. This is a good and simple approach for your need.
I wrote a piece of code to help or somelse how is trying to do it:
InkWell(
// can be gesture detector, button, etc
onTap: () => showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
color:
Colors.white60, // change it accordingly to you
height: 80, // change it accordingly to you
width: 200, // change it accordingly to you
child: Column(
children: [
const Text('Change your value here'),
TextField(
decoration:
InputDecoration(hintText: qty.toString()),
onChanged: (insertValue) => setState(() {
qty = int.parse(insertValue);
}),
// you can use other callBack function (like onComplete,
// onSaved), wich is more eficient than calling setState eveytime,
// but you have to do the needed adtaptions. Like onSave
// needs a key to call the save function. is easy just google it.
),
],
)),
)),
child: Text(
"${qty}",
),
),
What you are probably looking is a DropdownButton.
You would have something like this:
int qty = 1;
List<int> listOfValues = [1,2,3,4];
and then in your column you would have
DropdownButton<int>(
// This are the list of items that will appear in your dropdown menu.
// items is all the options you want your users to be able to select from,
// and it take a list of `DropdownMenuItem`. So instead of creating a `DropdownMenuItem`
// for each of the items in `listOfValues`, we iterate through it and return
// a `DropdownMenuItem`
items: listOfValues
.map((item) => DropdownMenuItem<int>(
value: item,
child: Text('$item'),
))
.toList(),
value: qty,
onChanged: (value) {
if (value != null) {
setState(() {
qty = value;
});
}
},
),
For more information on DropDownButton, check the following links:
https://api.flutter.dev/flutter/material/DropdownButton-class.html
https://www.youtube.com/watch?v=K8Y7sWZ7Q3s
Note: In a scenario where you want to increase the quantity of an item, like in a shopping cart, maybe having a button increment qty by 1 would be better.

How to change a state of a widget that is generated from list in flutter

My problem is simple. I was building a ListTile from the list of documents I get from firebase by iterating through the results. The ListTile contains a leading icon, a title, and a trailing favorite IconButton. The list tile shows perfectly as I want it to. But the problem arises when I try to change the color of the IconButton while a user taps on it. For some reason, the code I wrote isn't doing the trick. What i tried to do was to set the value of the IconButton's color by a ternary which uses a class variable named isFavorited. What i wanted it to do is change the color of the IconButton when i tap on that same IconButton. Here is my code block:
// Builds a tile for each brought up names of taxistops
if (retrievedData.isNotEmpty) {
retrievedData.forEach((element) {
if (element.contains(query) ||
element.contains(query.toUpperCase()) ||
element.contains(query.toLowerCase())) {
ListTile listTile = ListTile(
leading: Icon(Icons.local_taxi),
title: Text(element),
trailing: IconButton(
icon: isFavorited
? Icon(
Icons.star,
color: Colors.amber[400],
)
: Icon(Icons.star_border),
onPressed: () => {
setState(() {
isFavorited = true;
}),
addToFavorite()
},
),
onTap: () => close(context, element),
);
searchedTiles.add(listTile);
}
});
}
Any help is appreciated! Thank you in advance!
I think the problem is because you are adding the widget in the list you should preview it directly inside the widget father (ListView) so it can do the setState correctly and not inside a list you created as a data structure to store elements.
I think that's the issue if it still doesn't work I would need to see how you are showing the list.

Flutter toggle Radio widget

I'm using Radio widgets to set the user gender. These Radio button widgets are designed very well to handle the selection changes act accordingly, but I does not find a solution to somehow deselect one already selected option. I would like the gender radio group to be optional. But after user selects let's say female, he/she can change the selection to male, but can't deselect the female/male option. (So that none of the option is selected.)
There is only an onChanged property and it only changes if the gender changes. No such thing like onTap or something similar. So no matter if I check that gender if it's the same, onChanged won't be called.
For that reason I tried to use a GestureDetector which should solve this issue (with this I should be able to deselect the already selected option) but in case of radio widgets it does not work. I also tried to change it's behavior property but it did not help either.
Here is the function which I use to create gender radio option with a text in a Row.
Widget _buildRadioWithText(final genderChangeNotifier, final Gender genderParam, final String genderText) {
return Row(
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.translucent,
child: Radio<Gender>(
visualDensity: VisualDensity.compact,
value: genderParam,
groupValue: genderChangeNotifier.gender,
onChanged: (Gender gender) {
genderChangeNotifier.gender = gender;
print(gender.toString());
},
),
onTap: () {
if (genderChangeNotifier.gender == genderParam) {
genderChangeNotifier.gender = Gender.NA;
print("Not answered!");
}
},
),
GestureDetector(
onTap: () {
if (genderChangeNotifier.gender == genderParam) {
genderChangeNotifier.gender = Gender.NA;
print("Not answered!");
} else {
genderChangeNotifier.gender = genderParam;
print(genderParam.toString());
}
},
child: Text(
genderText,
style: TextStyle(
fontSize: _fontSize,
),
),
),
],
);
}
Function call:
_buildRadioWithText(genderChangeNotifier, Gender.FEMALE, "Female"),
_buildRadioWithText(genderChangeNotifier, Gender.MALE, "Male"),
genderChangeNotifieris just a provider to set and get the Gender value and notify listeners when a Gender is set.
final GenderNotifier genderChangeNotifier= Provider.of<GenderNotifier>(context);
GestureDetector's onTap works well when I tap on the Text widget. It selects then deselects the option just as I'd like to but in case of the Radio widget onTap is never called.
Any idea how to achieve the deselection when I tap/click on the Radio widget itself? And why the GestureDetector that wraps the Radio does not register the tap events?
The GestureDetector's onTap is not being called because the GestureDetector inside the Radio widget is taking priority. By default, when two GestureDetectors are competing for input, only one will "win". In this case, the winner is the one in the Radio. This article discusses "GestureArenas" and how you can allow both GestureDetectors to process input.
However, consider whether allowing the user to deselect the radio button is the correct solution. Radio buttons are designed not to be deselected once the user has selected an option. You might instead:
Offer a third, "I'd rather not say" option
Use toggleable buttons instead of a radio group
Use a dropdown menu instead of a radio group
See this answer for more info on the usability aspect of this.

Flutter: setState((){} fails to redraw DropdownButton value

I am new to flutter, and am trying to code mobile game. So far I have been able to manage through previous questions on forums, youtube tutorials and example apps. However I have hit a wall that I cannot solve.
Many of the features in my UI are supposed to change based on user behavior but do not update, I am using this DropdownButton as my example but it is a problem I am having elsewhere in the code as well. When the user makes a selection from the DropdownButton it should update the value of the button, but instead only the hint is displayed. I have been able to determine through print statements that the selection is registered.
Based on what I have learned so far I suspect that the issue entails my widget's statefulness or lack thereof. I found a few similar questions which suggested using a StatefulBuilder, however when I tried to implement it the code had errors or else duplicated my entire ListTile group. I also saw suggestions about creating a stateful widget instead but the instructions were too vague for me to follow and implement without errors. I am including snippets of the code below, I am hesitant to paste the whole thing because the app is nearly 2000 lines at this point. Please let me know if any further information or code is needed.
this is the problematic code, see below for the code in context.
DropdownButton(
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hunters Guild',
theme: ThemeData(
primaryColor: Colors.grey,
),
home: Main(),
);
}
}
final List<Combatant> _combatants = List<Combatant>();
//combatant is a class that holds stats like HP which may change during a battle
//they also have a Fighter and a Monster and one of those will be null and the other useful
//depending if faction = "good" or "evil
//I am aware that this may not be the most elegant implementation but that is a problem for another day.
class MainState extends State<Main> {
final List<Fighter> _squad = List<Fighter>();
//Fighter is a class which stores info like HP, name, skills, etc for game character,
//this List is populated in the page previous to _fight
//where the user picks which characters they are using in a given battle
void _fight(Quest theQuest){
for(Fighter guy in _squad){
_combatants.add(Combatant("good", guy, null));
}
_combatants.add(Combatant("evil", null, theQuest.boss));
//quests are a class which store a boss Monster, later on I will add other things like rewards and prerequisites
final tiles = _combatants.map((Combatant comba){ //this structure is from the build your first app codelab
List<DropdownMenuItem<String>> listDrop = [];
String skillChoice = null;
if (comba.faction == "good"){
for (Skill skill in comba.guy.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
else if (comba.faction == "evil"){
for (Skill skill in comba.boss.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
//^this code populates each ListTile (one for each combatant) with a drop down button of all their combat skills
return ListTile(
leading: comba.port,
title: Text(comba.info),
onTap: () {
if(comba.faction == "good"){
_fighterFightInfo(comba.guy, theQuest.boss); //separate page with more detailed info, not finished
}
else{
_monsterFightInfo(theQuest.boss); //same but for monsters
}
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton( //HERE IS WHERE THE ERROR LIES
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
IconButton(icon: Icon(Icons.check), onPressed: (){
//eventually this button will be used to execute the user's skill choice
})
],
),
subtitle: Text(comba.hp.toString()),
);
},
);
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
//I have tried moving most of the above code into here,
//and also implementing StatelessBuilders here and other places in the below code to no effect
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text("Slay "+theQuest.boss.name+" "+theQuest.boss.level.toString()+" "+theQuest.boss.type.name, style: TextStyle(fontSize: 14),),
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: () {
_squadSelect(theQuest);
}),
),
body: ListView(children: divided),
);
},
),
);
}
//there are a lot more methods here which I haven't included
#override
Widget build(BuildContext context) {
//I can show this if you need it but there's a lot of UI building on the main page
//Also might not be the most efficiently implemented
//what you need to know is that there are buttons which call _fight and go to that page
}
class Main extends StatefulWidget {
#override
MainState createState() => MainState();
}
Thank you for any help or advice you can offer.
Its a bit difficult to understand what is going on here, so if possible add the problem areas in a new widget and post it here.
In any case, for now it seems your skillShare variable is local, so every time you are setting state it does not update. Try something like this:
class MainState extends State<Main> {
String skillShare = ""; //i.e make skillShare a global variable
final List<Fighter> _squad = List<Fighter>();
The following question had information which helped me find the answer. Different question but the solution applies. Thanks user:4576996 Sven. Basically I need to reformat from doing my constructing in the void _fight to a new stateful page. This may be useful for anyone else who is using the beginning tutorial as a framework to build their app.
flutter delete item from listview