Build interactive flutter UI with class methods - flutter

I have a ListView of projects where each ListTile represents a project. For that I have a List of projects which is written into the ListView with a for loop. Each ListTile has an IconButton to define a project as favourite. To keep the code clean I defined methods inside the project class to build the UI. The reason behind that is that the project can be set as a favourite from different locations in the application.
Unfortunately I cannot get the application to update the UI depending on the favorite status. I suppose it has something to do with the return of the Widgets, as the state gets evaluated once on return and does not change until refresh.
I already extended the Project class with a ChangeNotifier without any result. Down below the code.
ListView(
children: [
for (var project in projects.projects) project.getListTile()
],
),
Project class with getListTile Method():
class Project extends ChangeNotifier{
bool favorite = false;
Project({this.favorite = false});
IconButton getFavoriteButton() {
return IconButton(
icon: favorite
? Icon(Icons.favorite_outlined)
: Icon(Icons.favorite_border_outlined),
color: favorite ? Colors.yellow : Colors.white,
onPressed: () {
favorite = !favorite;
notifyListeners();
},
);
}
ListTile getListTile() {
return ListTile(
leading: getFavoriteButton(),
),
);
}
}
Is there a way to refresh the UI, so that the favorite icon type and color updates on pressing the IconButton?

You should be using a StatefullWidget implementation.
These widgets are made to store a state and rebuild automatically on any state change when you do :
IconButton(
icon: favorite
? Icon(Icons.favorite_outlined)
: Icon(Icons.favorite_border_outlined),
color: favorite ? Colors.yellow : Colors.white,
onPressed: () {
setState((){
favorite = !favorite;
}
},
);

Related

What is MaterialStateProperty<Color>?

What is MaterialStateProperty in ButtonStyle?
ThemeData(
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: , //?
),
),
),
The purpose of MaterialStateProperty is to make it possible to specify different styles for different states.
For example, if we want a button that's usually blue, but turns green when it's pressed, and enlarges its texts at the same time, we can use MaterialStateProperty.resolveWith to do exactly that.
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
// If the button is pressed, return green, otherwise blue
if (states.contains(MaterialState.pressed)) {
return Colors.green;
}
return Colors.blue;
}),
textStyle: MaterialStateProperty.resolveWith((states) {
// If the button is pressed, return size 40, otherwise 20
if (states.contains(MaterialState.pressed)) {
return TextStyle(fontSize: 40);
}
return TextStyle(fontSize: 20);
}),
),
child: Text("Changing Button"),
onPressed: () {},
)
In addition to checking whether the button is being "pressed", MaterialStateProperty also supports: disabled, dragged, error, focused, hovered, pressed, scrolledUnder, selected. Note that it's possible to have multiple states at once. For example, a button can be both "disabled" & "hovered" at the same time. With MaterialStateProperty you can customize its appearance when that happens.
"Is there a clean way to resolve multiple states at once?"
The Flutter API documentation provides a nice clean pattern for resolving any of multiple states at one time. For example, if you want to respond the same way to all interactive states you could define a method like this:
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed, // Any states you want to affect here
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
// if any of the input states are found in our list
return Colors.blue;
}
return Colors.red; // default color
}
Then later in the button widget, simply assign that method as the resolver for the backgroundColor:
TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith(getColor),
),
onPressed: () {},
child: const Text('Changing Button'),
);
Play with the full example in Dart Pad.
"Okay, but I just want a red button."
Sure, it seems like you can use: MaterialStateProperty.all(Colors.red) to make it red in all cases. But that's probably NOT what you want. For example, when the button is disabled, do you still want it to be red?
See, "all" means "all". This is not good.
So what, are we stuck dealing with MaterialStateProperty and checking for disabled states all day?
Thankfully, no. There's a better way:
If you are using ElevatedButton, you can use ElevatedButton.styleFrom as a base style. Similarly, if you are using TextButton, you can use TextButton.styleFrom. From there, you can easily modify some of the styles.
Code:
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text("Red Button"),
onPressed: () {},
)
That's it, you just pass in a Color class. Super easy, no MaterialStateProperty involved. And it automatically handles edge cases for you.
I am assuming that you want to know how to assign a color to the backgroundColor parameter of the ButtonStyle widget. If that is the case then just type something like this:
backgroundColor: MaterialStateProperty.all(Colors.green),
OR
backgroundColor: MaterialStateProperty.all(Color(0xFF5D5F6E)),
Interface for classes that resolve to a value of type T based on a widget's interactive "state", which is defined as a set of MaterialStates.
Material state properties represent values that depend on a widget's material "state". The state is encoded as a set of MaterialState values, like MaterialState.focused, MaterialState.hovered, MaterialState.pressed. For example, the InkWell.overlayColor defines the color that fills the ink well when it's pressed (the "splash color"), focused, or hovered. The InkWell uses the overlay color's resolve method to compute the color for the ink well's current state.
ButtonStyle, which is used to configure the appearance of buttons like TextButton, ElevatedButton, and OutlinedButton, has many material state properties. The button widgets keep track of their current material state and resolve the button style's material state properties when their value is needed.
Code Example:
Widget build(BuildContext context) {
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue;
}
return Colors.red;
}
return TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith(getColor),
),
onPressed: () {},
child: Text('TextButton'),
);
}
A simple way to use it:
MaterialStateProperty.all(Colors.green) // Whatever value you want
To get more you can check official documentation of Material state properties made by the flutter team.
It is used to calculate the value depending on the current interactive state of the button, which can be hovered, pressed, focused,... (full list here).
If you want a fixed value, you can use MaterialStateProperty.all(YOUR_VALUE), this value will be applied to all button states.
You can find more information here: https://api.flutter.dev/flutter/material/MaterialStateProperty-class.html

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.

Finds Element without key

i'm still new in using flutter driver in testing, but as far as i know there are few identifiers that we can use to locate / identify elements, like By Text, By Type, etc
But the problem is, the app that i want to test doesn't have the identifier that i can use to locate them (please correct me if i'm wrong).. the widget code of the app looks like this
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => _controller.nextPage(),
),
),
);
}
where that widget is on a class that extends StatefulWidget.
How can i locate that icon in my test script and click it? can i use something like this? And what type of finder should i use? (byValueKey? bySemanticLabel? byType? or what?)
static final arrowKey = find.byValueKey(LoginKey.nextButton);
TestDriverUtil.tap(driver, arrowKey);
We have text and value checks here in Flutter Driver but if you don't have that you can always go the the hierarchy of app.
what I mean by hierarchy is so button has fix or specific parent right?
Let's take your example here, We have Align > Container > IconButton > Icon widget hierarchy which will not be true for others like there might be IconButton but not with the Container parent.
or StreamBuilder or anything that we can think of.
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => print("clicked button"),
),
),
);
}
This hierarchy should be atleast ideal for top bottom or bottom top approach.
Now what I mean by Top to bottom approach is Align must have IconButton and for bottom to up approach we are saying IconButton must have Align widget as parent.
Here i have taken top down approach so what I'm checking from below code is finding IconButton who is decendent of Align Widget.
also i added firstMatchOnly true as I was checking what happens if same hierarchy appears for both so
test('IconButton find and tap test', () async {
var findIconButton = find.descendant(of: find.byType("Align"), matching: find.byType("IconButton"), firstMatchOnly: true);
await driver.waitFor(findIconButton);
await driver.tap(findIconButton);
await Future.delayed(Duration(seconds: 3));
});
to check for multiple IconButtons with same Align as parent, we need to have some difference like parent should be having Text view or other widget.
find.descendant(of: find.ancestor(
of: find.byValue("somevalue"),
matching: find.byType("CustomWidgetClass")), matching: find.byType("IconButton"), firstMatchOnly: true)
usually I go something like above where I have split the code in seperate file and check for that widget.
But ultimately find something unique about that widget and you can work on that.
**In Lib directory dart class for connecting that widget**
class Testing extends StatelessWidget {
Testing();
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: YourClass(), // Next button containing class that need to test
);
}
}
**In Test directory**
testWidgets('Next widget field test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(Testing());
// find Widget
var buttonFind = find.byIcon(Icons.arrow_forward);
expect(buttonFind, findsOneWidget);
IconButton iconButton = tester.firstWidget(buttonFind);
expect(iconButton.color, Colors.blue);
});

Flutter identify Widget built by loop or listview.builder

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.

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