Flutter Expansion Pannel not Expanding without immutable bool - flutter

I have an expansion panel in _buildCategoryListings() that does not expand when the header or the dropdown button is clicked. isExpanded is set to the boolean categoryView.isExpanded. Through printing via the console I can see that the setState is actually updating the bool value but it looks like the actual widget isn't being redrawn perhaps? If I manually set isExpanded to true I see the results I want from the GUI. I also had set isExtended to theExpanded (which is in MovieListingView) which raises the issue of a mutable variable being in a class that extends StatefulWidget, this did give me the desired results though.
The question: How do I get the expansion panel to update the categoryView.isExpanded (via theListings[panelIndex].isExpanded) bool and show it via the GUI?
Thank you in advance.
Side note I thought about using a provider to keep track of this bool but that seems like overkill.
class MovieListingView extends StatefulWidget {
#override
_MovieListingView createState() => _MovieListingView();
MovieListingView(this.movieList);
final MovieCatalog movieList;
//bool theExpanded = false;
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = movieList.Categories;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList: movieList.getMovieCardListByCategory(category));
tempList.add(categoryView);
});
return tempList;
}
}
class _MovieListingView extends State<MovieListingView> {
Widget build(BuildContext context) {
// TODO: implement build
return SingleChildScrollView(
physics: ScrollPhysics(),
padding: EdgeInsets.all(5.0),
child: _buildCategoryListings(),
);
}
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = widget.movieList.Categories;
int counter = 0;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList:
widget.movieList.getMenuItemCardListByCategory(category),
isExpanded: false);
tempList.add(categoryView);
});
return tempList;
}
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
return ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
theListings[panelIndex].isExpanded = !isExpanded;
//widget.theExpanded = !isExpanded;
});
},
children: theListings.map((MovieCategoryView movieCategoryView) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(movieCategoryView.movieCategoryName),
);
},
body: Column(
children: movieCategoryView.movieList,
),
isExpanded: movieCategoryView.isExpanded);
}).toList(),
);
}
}
class MovieCategoryView {
MovieCategoryView(
{#required this.movieCategoryName,
#required this.movieList,
this.isExpanded});
String movieCategoryName;
List<MovieCard> movieList;
bool isExpanded = false;
}

This is happening because whenever the setstate() is called whole widget tree is rebuild and thus when you try changing the isexpandable value ,is gets changed but the
function generateCategoryList(); again gets called and generates the previous list again and again.
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
To fix this call the generateCategoryList(); once in initState() and remove the line above line.

Related

Set state for a button in a ListView

I have an input page to create a new reminder. On this page you will select several different variables (reminder type, start date, etc.) - at this stage I am just trying to get it work for two variables.
I have a button object that I create, which takes some text, a "isSelected" value (which changes the color to show it is selected) and an onPress callback. The plan is to use a loop to create one of these buttons for each of the necessary selection options and then feed that into a ListView, so you have a scroll-able list of selection option. As you select the item the properties of the new reminder object will update and the color will change to selected.
When I click the button, the value is selected (the print statement shows this) but the button does not change to the new isSelected value, despite a SetState being used. What is it I am missing here? Is it possible to feed buttons into a ListView like this and still have their state update? Or do you need to find another work around?
class AddReminder extends StatefulWidget {
#override
_AddReminderState createState() => _AddReminderState();
}
class _AddReminderState extends State<AddReminder> {
String addReminder = "";
Reminder newReminder = Reminder();
#override
List<Widget> getReminderTypesButton(
String selectionName, List selectionOptions, var reminderVariable) {
// create new list to add widgets to
List<Widget> selectionOptionsWidgets = [];
// loop through selection options and create buttons
for (String selection in selectionOptions) {
bool isSelectedValue = false;
selectionOptionsWidgets.add(
FullWidthButton(
text: selection,
isSelected: isSelectedValue,
onPress: () {
setState(() {
reminderVariable = selection;
isSelectedValue = true;
});
print(reminderVariable);
},
),
);
}
;
// return list of widgets
return selectionOptionsWidgets;
}
Widget build(BuildContext context) {
List<List<Widget>> newList = [
getReminderTypesButton("Type", reminderTypesList, newReminder.type),
getReminderTypesButton(
"Frequency", repeatFrequencyTypesList, newReminder.repeatFrequency)
];
List<Widget> widgetListUnwrap(List<List<Widget>> inputList) {
//takes list of list of widgets and converts to widget list (to feed into list view)
List<Widget> widgetsUnwrapped = [];
for (var mainList in inputList) {
for (var widgets in mainList) {
widgetsUnwrapped.add(widgets);
}
}
return widgetsUnwrapped;
}
return SafeArea(
child: Container(
color: Colors.white,
child: Column(
children: [
Hero(
tag: addReminder,
child: TopBarWithBack(
mainText: "New reminder",
onPress: () {
Navigator.pop(context);
},
)),
Expanded(
child: Container(
child: ListView(
children: widgetListUnwrap(newList),
shrinkWrap: true,
),
),
),
],
),
),
);
}
}
Here are the lists that I reference
List<String> reminderTypesList = [
"Appointment",
"Check-up",
"Other",
];
List<String> repeatFrequencyTypesList = [
"Never",
"Daily",
"Weekly",
"Monthly",
"Every 3 months",
"Every 6 months",
"Yearly",
];
List<List<String>> selectionOptions = [
reminderTypesList,
repeatFrequencyTypesList
];
The reason your state not changing is that every time you call setState(), the whole build function will run again. If you initiate a state (in this case isSelectedValue) within the build method (since the getReminderTypesButton() got called within the build), the code will run through the below line again and again, resetting the state to the initial value.
bool isSelectedValue = false;
This line will always set the isSelectedValue to false, no matter how many time you call setState.
In order to avoid this, you need to place the state outside of the build method, ideally in the FullWidthButton like this:
class FullWidthButton extends StatefulWidget {
const FullWidthButton({Key? key}) : super(key: key);
#override
_FullWidthButtonState createState() => _FullWidthButtonState();
}
class _FullWidthButtonState extends State<FullWidthButton> {
bool isSelectedValue = false;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () => setState(() => isSelectedValue = !isSelectedValue),
// ...other lines
);
}
}

How change value item in CheckBoxListTile?

I am trying to update the check of the CheckBoxListTile but I am not getting the desired result.
This is my code:
Widget build(BuildContext context) {
return Query(
options: QueryOptions(document: getVehiclesTypeQueryDoc),
builder: (result, {fetchMore, refetch}) {
if (result.isLoading && result.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
newVehiclesType = [];
final _vehiclesTypeJson = result.data['tipoVehiculos'] as List<dynamic>;
for (final i in _vehiclesTypeJson) {
var productMap = {
'id': i['id'],
'nombre': i['nombre'],
'imagenActivo': i['imagenActivo'],
'isSelected': true
};
newVehiclesType.add(productMap);
}
final List<dynamic> test = newVehiclesType;
print('test: $test');
return ListView.builder(
itemCount: test.length,
shrinkWrap: true,
itemBuilder: (context, index) {
print(index);
print('VALO: ${test[index]['isSelected']}');
return CheckboxListTile(
title: Text(test[index]['nombre'].toString()),
value: test[index]['isSelected'],
onChanged: (bool newValue) {
setState(() {
test[index]['isSelected'] = newValue;
});
},
);
},
);
},
);
}
I created a newVehiclesType variable, the query did not give me a variable to use the check for.
I am new to flutter.
this is the code that i use in my application hopefully hopefully it will help you
value: isCheckedEdit.contains(_ingredientList[index].nameIngredient),
onChanged: (value) {
if (value) {
setState(() {
isCheckedEdit
.add(_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
} else {
setState(() {
isCheckedEdit.remove(
_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
}
},
);
Angular has an amazing documentation that teaches every concept in detail (60%). I highly recommend you to visit the the documentation site. There are examples and explanations for each topic.
https://angular.io/guide/reactive-forms
Thank you so much for bringing such a beautiful question up. Angular is love.
class VehiclesList extends StatefulWidget {
#override
_VehiclesListState createState() => _VehiclesListState();
}
class _VehiclesListState extends State<VehiclesList> {
List<dynamic> test;
#override
Widget build(BuildContext context) {
// your code
// when result has data, instead of
// final List<dynamic> test = newVehiclesType;
// do this
test = newVehiclesType;
}
}
What this basically does is, it uses test to hold the widget state. In your code, test is local to your build function and would be reinitialized as many times build function runs.
This way it would not reinitialize on widget rebuilds when setState() is called.

Single Selection for ListView Flutter

I am trying to implement a listView single selection in my app such that once an item in the list is tapped such that pressed item color state is different from the others. I have done all I know but it does not work well. The problem is that even though my implementation updates each item state when pressed, it doesn't reset the others to their initial state.
class BoxSelection{
bool isSelected;
String title;
String options;
BoxSelection({this.title, this.isSelected, this.options});
}
class _AddProjectState extends State<AddProject> {
List<BoxSelection> projectType = new List();
#override
void didChangeDependencies() {
super.didChangeDependencies();
projectType
.add(BoxSelection(title: "Building", isSelected: false, options: "A"));
projectType
.add(BoxSelection(title: "Gym House", isSelected: false, options: "B"));
projectType
.add(BoxSelection(title: "School", isSelected: false, options: "C"));
}
child: ListView.builder(
itemCount: projectType.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
//here am trying to implement single selection for the options in the list but it don't work well
for(int i = 0; i < projectType.length; i++) {
if (i == index) {
setState(() {
projectType[index].isSelected = true;
});
} else {
setState(() {
projectType[index].isSelected = false;
});
}
}
});
},
child: BoxSelectionButton(
isSelected: projectType[index].isSelected,
option: projectType[index].options,
title: projectType[index].title,
),
);
},
),
Your problem is that you're using index to access projectType elements but you should be using i
if (i == index) {
setState(() {
projectType[i].isSelected = true;
});
} else {
setState(() {
projectType[i].isSelected = false;
});
}
In any case I think your code can be improved since it's not as efficient as it could be. You're iterating over the entire list and calling setState twice in every iteration, recreating the widget tree a lot of times unnecessarily when it can be done in one shoot.
Save your current selection in a class level variable
BoxSelection _selectedBox
Simplify your code to act directly over the current selection insted of iterating over the entire list
onTap: () =>
setState(() {
if (_selectedBox != null) {
_selectedBox.isSelected = false;
}
projectType[index].isSelected = !projectType[index].isSelected;
_selectedBox = projectType[index];
});

Flutter Provider: How to notify a model that a change happened on a model it contains?

I'm starting to learn Flutter/Dart by building a simple Todo app using Provider, and I've run into a state management issue. To be clear, the code I've written works, but it seems... wrong. I can't find any examples that resemble my case enough for me to understand what the correct way to approach the issue is.
This is what the app looks like
It's a grocery list divided by sections ("Frozen", "Fruits and Veggies"). Every section has multiple items, and displays a "x of y completed" progress indicator. Every item "completes" when it is pressed.
TheGroceryItemModel looks like this:
class GroceryItemModel extends ChangeNotifier {
final String name;
bool _completed = false;
GroceryItemModel(this.name);
bool get completed => _completed;
void complete() {
_completed = true;
notifyListeners();
}
}
And I use it in the GroceryItem widget like so:
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
GroceryItem(this.model);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryItemModel>(builder: (context, groceryItem, child) {
return ListTile(
title: Text(groceryItem.name),
leading: groceryItem.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked)
onTap: () => groceryItem.complete();
})
);
}
}
The next step I want is to include multiple items in a section, which tracks completeness based on how many items are completed.
The GroceryListSectionModel looks like this:
class GroceryListSectionModel extends ChangeNotifier {
final String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
// THIS RIGHT HERE IS WHERE IT GETS WEIRD
items.forEach((item) {
item.addListener(notifyListeners);
});
// END WEIRD
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
}
And I use it in the GroceryListSection widget like so:
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
final ValueChanged<bool> onChanged;
GroceryListSection(this.model, this.onChanged);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.map((groceryItemModel) =>
GroceryItem(groceryItemModel)).toList()
)
);
}
)
);
}
}
The Problems:
It seems weird to have a ChangeNotifierProvider and a Consumer in both Widgets. None of the examples I've seen do that.
It's definitely wrong to have the GroceryListSectionModel listening to changes on all the GroceryItemModels for changes to propagate back up the tree. I don't see how that can scale right.
Any suggestions? Thanks!
this ist not a nested Provider, but i think in your example it is the better way..
only one ChangeNotifierProvider per section ("Frozen", "Fruits and Veggies") is defined
the complete() function from a ItemModel is in the GroceryListSectionModel() and with the parameter from the current List Index
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
// final ValueChanged<bool> onChanged;
GroceryListSection(this.model);
#override
Widget build(BuildContext context) {
return new ChangeNotifierProvider<GroceryListSectionModel>(
create: (context) => GroceryListSectionModel(model.name, model.items),
child: new Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.asMap().map((i, groceryItemModel) => MapEntry(i, GroceryItem(groceryItemModel, i))).values.toList()
)
);
}
)
);
}
}
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
final int index;
GroceryItem(this.model, this.index);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(model.name),
leading: model.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked),
onTap: () => Provider.of<GroceryListSectionModel>(context, listen: false).complete(index),
);
}
}
class GroceryListSectionModel extends ChangeNotifier {
String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
// complete Void with index from List items
void complete(int index) {
this.items[index].completed = true;
notifyListeners();
}
}
// normal Model without ChangeNotifier
class GroceryItemModel {
final String name;
bool completed = false;
GroceryItemModel({this.name, completed}) {
this.completed = completed == null ? false : completed;
}
}

Why can't I load actual data in the expansion panels in flutter of "Only static members can be accesed in initializers"?

I looked at the official docs flutter expansion panels as a reference to building my own. I'm having trouble making them work AND work with my data. I have tried passing some of the data my widget receives in its constructor to be used when the panel list is being generated. I hope this code fragments are enough, if not I can add more.
Here is where the error pops up (when trying to reference widget.contracts)
class FinancialTabContent extends StatefulWidget {
final List<Contract> contracts;
final Person travelOfficer;
FinancialTabContent({this.contracts, this.travelOfficer});
#override
_FinancialTabContentState createState() => _FinancialTabContentState();
}
class _FinancialTabContentState extends State<FinancialTabContent> {
List<ExpandableItem> expansionPanels = generateExpansionPanels(widget.contracts);
#override
Widget build(BuildContext context) {
...
This is what I intended to use to build the panel list, it's placed inside the _FinancialTabContentState
Widget _buildPanelList() {
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
expansionPanels[index].isExpanded = !isExpanded;
});
},
children: expansionPanels.map<ExpansionPanel>((ExpandableItem item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
....
isExpanded: item.isExpanded,
);
}).toList(),
);
Here's the code for the expandable item
import 'contract.dart';
class ExpandableItem {
ExpandableItem({
this.isExpanded = false,
this.panelData
});
bool isExpanded;
Contract panelData;
}
List<ExpandableItem> generateExpansionPanels(List<Contract> panelsData) {
return List.generate(panelsData.length, (int index) {
return ExpandableItem(
panelData: panelsData[index],
);
});
}
Move this line to within the build method:
List<ExpandableItem> expansionPanels = generateExpansionPanels(widget.contracts);
Or do this in the initState method if you want it only done once:
class _FinancialTabContentState extends State<FinancialTabContent> {
List<ExpandableItem> expansionPanels;
#override
void initState() {
super.initState();
expansionPanels = generateExpansionPanels(widget.contracts);
}
#override
Widget build(BuildContext context) {...}
}
Read the docs on State which describe in detail the lifecycle of a Widget.