Flutter Item ExpansionPanelList doesn't change state - flutter

I am trying to retrieve data from API, that's works nice.
After that I want to show my data in a ExpansionPanelList, which is builded by a method:
class _CartaPageState extends State<CartaPage> {
#override
Widget build(BuildContext context) {
// Nos suscribimos al provider
final productoService = Provider.of<ProductoService>(context);
final List<Producto> productos = productoService.productos;
_productosItems = productosToItem(productos);
return Scaffold(
body: Container(
height: double.infinity,
width: double.infinity,
child: ListView(
children: [
ExpansionPanelList(
animationDuration: Duration(milliseconds: 300),
expansionCallback: (int index, bool isExpanded) {
setState(() {
_productosItems[index].isExpanded = !isExpanded;
//productosItems[index].isExpanded = !productosItems[index].isExpanded;
});
},
//children: productosToItem(productoService.entrantes).map<ExpansionPanel>((Item item) {
children: _productosItems.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
headerBuilder: (context, isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
................
The data is shown perfect, but the state is not refreshing on my ItemModel, I think the problem is because the widget is redrawing each time I touch the panel list, that retrieve (again) data from the API and never changes the state.
How can I resolve it?
Thank you in advance
EDIT: CartaPage is wraped by:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ProductoService()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Material App',
home: CartaPage()
),
);
}
}
EDIT 2:
I agree I am losing state, this it the method to convert Product to Item:
List<Item> productosToItem(List<Producto> productos) {
return List.generate(productos.length, (index) {
return Item(
headerValue: productos[index].tipo,
expandedValue: productos[index].nombre,
);
});
}

Is ExpansionPanel having its isExpanded set to item.isExpanded?
You get your isExpanded state from whatever productosToItem() generates.
When you call setState you queue a new build, which will call productosToItem() again. Without knowing what that method does, I cannot help much.
I would suggest you look into productosToItem and why it isn't setting isExpanded to the correct value.
If _productosItems[index].isExpanded isn't a setter, I would imagine you are losing the state.
EDIT 1:
You can create an internal state list that can persist the expanded state:
class Item {
Item({
this.expandedValue,
this.headerValue,
this.producto,
this.isExpanded = false,
});
String expandedValue;
String headerValue;
Producto producto; // <------------- ADDED
bool isExpanded;
}
class _CartaPageState extends State<CartaPage> {
Map<Producto, bool> expanded = {}; // <------------- ADDED
#override
Widget build(BuildContext context) {
// Nos suscribimos al provider
final productoService = Provider.of<ProductoService>(context);
final List<Producto> productos = productoService.productos;
// NOTE: ----------- converted to a local variable
final _productosItems = productosToItem(productos);
return Scaffold(
body: Container(
height: double.infinity,
width: double.infinity,
child: ListView(
children: [
ExpansionPanelList(
key: ValueKey(productos.length), // <------------- ADDED
animationDuration: Duration(milliseconds: 300),
expansionCallback: (int index, bool isExpanded) {
// NOTE: ----------- updated
final producto = productos[index];
setState(() {
expanded[producto] = !isExpanded;
});
},
children: _productosItems.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
isExpanded: expanded[item.producto], // <------------- ADDED
canTapOnHeader: true,
headerBuilder: (context, isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
body: ListTile(
title: Text(item.expandedValue),
),
);
}).toList(),
),
],
),
),
);
}
List<Item> productosToItem(List<Producto> productos) {
// keep a list of previous map
final toRemove = Map<Producto, bool>.from(expanded);
final items = List.generate(productos.length, (index) {
final producto = productos[index];
// set initial expanded state
expanded.putIfAbsent(producto, () => false);
// the item will be retained
toRemove.remove(producto);
return Item(
headerValue: producto.tipo,
expandedValue: producto.nombre,
producto: producto,
isExpanded: expanded[producto],
);
});
if (toRemove.isNotEmpty) {
// cleanup unused items
expanded.removeWhere((key, _) => toRemove.containsKey(key));
}
return items;
}
}
The key: ValueKey(productos.length), is needed, since ExpansionPanelList acted weirdly with magically appearing or disappearing items.

Related

Is there a way to remove a widget from the tree after button press?

Flutter newbie here. I'm trying to make a simple to-do list that I'm using to build up my skills. The idea is there is a to-do list that you can move the properties up and down and once you complete a task you check it off and it should display a checkmark and maybe eventually play an animation and remove it. For now, I'm stuck on just removing it, I had a few implementation ideas, but a lot of them will require me to restart my code. Is there a way to make it so that once I am done with a specific widget I can delete it or replace it with another, noting the fact that these widgets are in a list? code for reference:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
var checkboxes = [
Checkboxstate(title: "test"),
Checkboxstate(title: "test2"),
Checkboxstate(title: "test3"),
Checkboxstate(title: "tes4t"),
];
bool value = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
children: [
...checkboxes.map(buildBox).toList(),
],
),
),
);
}
Card buildBox(Checkboxstate checkbox) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkboxes.removeAt();
checkbox.value = value;
});
},
),
);
}
}
class Checkboxstate {
String title;
bool value;
Checkboxstate({this.title, this.value = false});
}
EDIT:
As suggested by Prabhanshu I followed his steps and instead used the item builder; however, there is a new issue: the reorderablelistview now doesn't work. My idea was that the reason was that the index was different for the reorderable list than the checkbox widget I create, but I am still unable to find a solution.
new code:
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
var checkboxes = [
Checkboxstate(title: "test"),
Checkboxstate(title: "test2"),
Checkboxstate(title: "test3"),
Checkboxstate(title: "tes4t"),
];
bool value = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView.builder(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
itemCount: checkboxes.length,
itemBuilder: (context, index) {
Checkboxstate box = checkboxes.elementAt(index);
return buildBox(box, index);
},
),
),
);
}
Card buildBox(Checkboxstate checkbox, int index) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkbox.value = value;
checkboxes.removeAt(index);
});
},
),
);
}
}
class Checkboxstate {
String title;
bool value;
Checkboxstate({this.title, this.value = false});
}
If drag to dismiss is an option:
What you're looking for here is the Dismissible Widget, which as the docs describe is a widget that can be dismissed by dragging in the indicated direction.
Check out the flutter cookbook tutorial for a detailed explanation
on how to use this widget.
This will definitely help you.
replace the build method body with this
Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView.builder(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
itemCount: checkboxes.length,
itemBuilder: (context, index) {
Checkboxstate box = checkboxes.elementAt(index);
return buildBox(box, index);
},
),
),
);
and
Replace this method also
Card buildBox(Checkboxstate checkbox, int index) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkboxes.removeAt(index);
checkbox.value = value;
});
},
),
);
}

Passing a value from child to parent and then rebuild the state

I try to pass the value of my carouselwidget index to another widget . however the value returned in my second widget is null ( so for example if i drag a value in my carousel the getindex function prints the index value but the print test in the background_widget stay initialize as null
I think the pb is that i trie to passe a variable (index) from a child (carousel) to an another (Map) through the common parent (Screen stacking carousel and map together).
I'm trying to pass the index now with help of a provider but without success (and seems really overkill to build a model & a provider just to pass a f**ing variable between too child at the same level):
import 'package:provider/provider.dart';
import 'package:algua_alpha/provider/provideIndex.dart';
/*All the variables definition are not published here */
//// model class for index
class Index {
final int id;
Index(this.id);
}
class ProvideIdx with ChangeNotifier {
Index _index;
Index get index {
print(_index);
return _index;
}
void changeId() {
notifyListeners();
}
}
class RegionSelection extends StatefulWidget {
final String region;
RegionSelection(this.region);
#override
_RegionSelectionState createState() => _RegionSelectionState(region);
}
class _RegionSelectionState extends State<RegionSelection> {
// main screen where Carousel and map are stacked
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (ctx) => ProvideIdx(),
child: Scaffold(
body: Container(
child:!isLoading?
Stack(
children: <Widget>[
Maposm(region,// this widget need the Index of the carousel slider
),
Align(
alignment: Alignment.bottomCenter,
child: CarouselSlider.builder( //========= Carousel
itemBuilder: (BuildContext c, index) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => NewScreen(
objectlist: region,
item: index,
)));
},
);
itemCount: region.length,
options: CarouselOptions(
enableInfiniteScroll: true,
scrollDirection: Axis.horizontal,
enlargeCenterPage: true,
onPageChanged: (index, reason) { // this callback initialize the new index value
setState(() {
Index(index);
});
},
);
}
)
])
: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.white,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.orange[300]))
),
);
}
}
//=============================== The Map ===============================
class Maposm extends StatefulWidget {
//final carouselID;
final region;
Maposm(this.region);
#override
_MaposmState createState() =>_MaposmState(region);
}
class _MaposmState extends State<Maposm> {
#override
Widget build(BuildContext context) {
final indexes = Provider.of<Index>(context);
final carouselID = indexes.id; //get carousel id
List<Marker> markers = [];
for (var i = 0; i < region.length; i++) {
var m = Marker(
point:
LatLng(region[i].xy[0], region[i].xy[1]),
builder: (ctx) => InkWell(
child: (carouselID == i // index of marker builder == index of carousel
? Icon(
Icons.assistant,
color: Colors.pink[700],
size: 20,
)
: Icon( // else
Icons.brightness_1_sharp,
color: Colors.black87,
size: 4,
)),
onDoubleTap: null,
));
markers.add(m);
}
return FlutterMap(
options: MapOptions(
interactive: true,
slideOnBoundaries: true,
bounds: LatLngBounds(
LatLng(coordxy[0], coordxy[1]), LatLng(coordxy[2], coordxy[3])),
),
layers: [
TileLayerOptions(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c']),
MarkerLayerOptions(markers: markers)
]);
}
}

Get index of an Item from an other List

I started to use providers but I have a problem. I want to get the index of items that are in an other list in an other screen. How can i get themĀ ? I have two screens: a home screen and a favorite screen and I have a listView in each. I want to get the index of the item in the home screen when it is remove from the favorite screen. This is the link of my code on GitHub : https://github.com/Rianou20/my_app_from_scratch/tree/master/my_app_from_scratch. And some relevant parts of my code :
favModel.dart
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
addInFavorite(title, description, index){
Item item = Item(title: title, description: description, );
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
notifyListeners();
}
implement(){
isInFav.add(false);
}
}
favorite_screen.dart
class Favorite extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Favorite'),
),
body: Consumer<FavModel>(
builder: (context, favModel, child) {
return ListView.builder(
itemCount: favModel.favList.length,
itemBuilder: (context, index) {
return TextObject(favModel.favList[index].title,
favModel.favList[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
Icons.favorite,
color: Colors.red,
size: 32,
),
onTap: () {
favModel.removeOfFavorite(index, index);
}),
),
});
},
),
);
}
}
home_screen.dart
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return Favorite();
},
),
),
),
],
),
body: Consumer<FavModel>(builder: (context, favModel, child) {
return ListView.builder(
shrinkWrap: false,
itemCount: itemData.length,
itemBuilder: (context, index) {
favModel.implement();
return TextObject(
itemData[index].title, itemData[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
favModel.isInFav.elementAt(index)
? Icons.favorite
: Icons.favorite_border,
color:
favModel.isInFav[index] ? Colors.red : null,
size: 32,
),
onTap: () {
favModel.isInFav[index]
? null
: Provider.of<FavModel>(context,
listen: false)
.addInFavorite(
itemData[index].title,
itemData[index].description,
index,
);
}),
);
});
}),
);
}
}
Where I want to get the index is in the favorite_screen.dart at this line favModel.removeOfFavorite(index, index);
Without knowing the exact use case, you can potentially store the removed values in a list and use them on your home screen.
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
List<int> _removedItemIndexList = []
get removedItemIndexList => _removedItemIndexList;
addInFavorite(title, description, countdown, imageURL, index){
Item item = Item(title: title, description: description, countdown:countdown, imageURL: imageURL);
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
_addToRemovedIndexList(index);
notifyListeners();
}
void _addToRemovedIndexList(int index) {
_removedItemIndexList.add(index);
}
implement(){
isInFav.add(false);
}
}
And then use on home_sreen.dart as
...
body: Consumer<FavModel>(builder: (context, favModel, child) {
List<int> removedIndexes = favModel.removedItemIndexList;
return ListView.builder( ... ) };
Note that the FavModel provider class must be lifted above then home_screen.dart on the widget tree in order to be able to access its values. i.e. you would want to do something like this in your main.dart
...
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: FavModel(),
),
],
child: MaterialApp(...

Flutter Checkbox not changing/updating/working

I am trying to learn checkboxes in Flutter.
The problem is, when I want to use checkboxes in Scaffold(body:) it is working. But I want to use it in different places like an item in ListView.
return Center(
child: Checkbox(
value: testValue,
onChanged: (bool value) {
setState() {
testValue = value;
}
},
));
But it is not working, updating and changing anything.
Edit: I solved my problem with putting checkbox in a StatefulBuilder. Thanks to #cristianbregant
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
});
Try these maybe:
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
and remember that if you are using it in a dialog or bottomsheet you need to wrap the Checkbox Widget in a Stateful builder because the state does not update.
Checkboxes require you have a Scaffold or Material as their parent. Without either of these, you get this helpful error message:
The following assertion was thrown building Checkbox(dirty, state: _CheckboxState#1163b):
No Material widget found.
Checkbox widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material.
In Flutter's material library, that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. Because of this, many material library widgets require that there be a Material widget in the tree above them.
Once you have a material ancestor, you can place the ListView as it's child and it should show fine:
class SettingsPage extends StatefulWidget {
#override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
var _foo = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Toggle Foo'),
Checkbox(
value: _foo,
onChanged: (bool value) {
setState(() => _foo = value);
},
),
],
),
],
),
);
}
}
Seems like you will have to use both initState and dispose.
See my code example below:
class SettingsOrder extends StatefulWidget {
#override
_SettingsOrderState createState() => _SettingsOrderState();
}
class _SettingsOrderState extends State<SettingsOrder> {
List options = [];
List<bool> newoptions = [];
int selectedoption;
bool checkedstatus;
bool initialcall;
Future getproductlist(selectedoption, checkedstatus, initialcall) async{
List updatedlist = [];
final arguments = ModalRoute.of(context).settings.arguments as Map;
int itempos = 0;
options.clear();
if(initialcall == false){
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : newoptions[itempos]
});
itempos++;
}
} else {
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : checkedstatus
});
newoptions.add(false);
itempos++;
}
}
setState(() {
options = updatedlist;
});
}
#override
void initState(){
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
getproductlist(0, false, true);
return Scaffold(
body: SingleChildScrollView(
child: Container(
width: double.infinity,
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index){
return Container(
child: Theme(
data: ThemeData(
unselectedWidgetColor: Colors.grey
),
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(options[index]['name']),
value: options[index]['checkbox'],
onChanged: (newvalue){
int indexposition = index;
newoptions.removeAt(indexposition);
newoptions.insert(indexposition, newvalue);
getproductlist(indexposition, newvalue, false);
},
activeColor: Color.fromRGBO(0, 130, 214, 1),
checkColor: Colors.white,
),
),
);
}
),
),
),
);
}

Flutter avoid widget rebuild on collapsing/expanding ExpansionPanelList

In ExpansionPanelList, I have a problem of rebuilding widget when I expand/collapse it.
The problem is here:
expansionCallback: (int index, bool isExpanded) {
setState(() {
_profileExpansionStateMap[_profileExpansionStateMap.keys.toList()[index]] = !isExpanded;
});
},
I changed it to use Bloc state management to solve, but that has same behavior with setState(). Is there any way to avoid rebuilding widget tree? I can't use Selector widget as I don't think it would help me here.
import 'package:flutter/material.dart';
void main()=>runApp(MaterialApp(home: Home(),));
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() =>HomeState();
}
class HomeState extends State<Home> {
Map<String, bool> _profileExpansionStateMap = Map<String, bool>();
#override
void initState() {
super.initState();
_profileExpansionStateMap = {
"UserInformation": false,
"UserWeight": false,
"UserGeneralInformation": false,
};
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:Text('ExpansionPanel')),
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(20.0),
child:ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_profileExpansionStateMap[_profileExpansionStateMap.keys.toList()[index]] = !isExpanded;
});
},
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
height: 80.0,
child: Text('aaaaaaaa'),
);
},
body: Container(child:Text('aaaaaaaa')),
isExpanded: _profileExpansionStateMap["UserInformation"]),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
height: 80.0,
child: Text('bbbbbbbbbbbb'),
);
},
body: Container(child:Text('bbbbbbbbbbbb')),
isExpanded: _profileExpansionStateMap["UserWeight"]),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
height: 80.0,
child: Text('ccccccccc'),
);
},
body: Container(child:Text('ccccccccc')),
isExpanded: _profileExpansionStateMap["UserGeneralInformation"]),
],
)
),
),
);
}
}
ExpansionPanelList.expansionCallback(...) is a method that gets called whenever you tap on arrow buttons inside your ExpansionPanelList to expand/collapse it.
In this method you are actually supposed to setup your bool values passed to isExpanded of ExpansionPanel thus requiring you to call setState(...).
If you, however, have issues with this, then it clearly indicates there is something wrong with your code. So, there is no need to avoid rebuilding the widget state.