Close an ExpansionTile when another ExpansionTile is tapped - flutter

I have a list of ExpansionTile with a list of ListTile in a Drawer. What I want to achieve is, when I press an ExpansionTile, the another ExpansionTile must be collapsed. I had been stuck with this problem for two days and could not find an answer. Can anybody know how to collapse the ExpansionTile programmatically?
Note:
I don't want to mess up the animation of the widget.
Here is my code,
ListView.builder(
itemCount: userList.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, findex) {
return ExpansionTile(
key: Key(findex.toString()),
title: Text(userList[findex].parentdata[0].title,
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold,color: Colors.black),
),
onExpansionChanged: (value) {
},
children: [
ListView.builder(
itemCount: userList[findex].document.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, sindex) {
return ListTile(
title: Text(
userList[findex].document[sindex].title,
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold,color: Colors.black),
),
onTap: () {
print(
userList[findex].document[sindex].title);
},
);
},
),
],
);
},
),

Try below code
declare one int variable
int selectedTile = -1;
Your widget
ListView.builder(
key: Key(selectedTile.toString()),
itemCount: 5,
itemBuilder: (context, index) {
return ExpansionTile(
key: Key(index.toString()),
initiallyExpanded: index == selectedTile,
title: Text('ExpansionTile $index'),
subtitle: Text('Trailing expansion arrow icon'),
children: [
ListTile(
title: Text('This is tile number $index'),
),
],
onExpansionChanged: ((newState) {
if (newState)
setState(() {
selectedTile = index;
});
else
setState(() {
selectedTile = -1;
});
}),
);
},
);

Use ExpansionPanel widget.
You need to create a variable and maintain the expansion state of expansion panel index.
expansionCallback: (int index, bool isExpanded) {
setState(() {
// when any of expansionPanel is Tapped
// set all expansion to false
for(int i = 0; i<_data.length; i++){
_data[i].isExpanded = false;
}
// then set the tapped index to its state
_data[index].isExpanded = !isExpanded;
});
},
Here is an live demo for expansion panel

Try this:
Create a variable: int selected = -1;
And listview:
ListView.builder(
itemCount: 10,
shrinkWrap: true,
itemBuilder: (BuildContext context, findex) {
return ExpansionTile(
initiallyExpanded: findex == selected,
key: Key(selected.toString()),
title: Text(userList[findex].parentdata[0].title,
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold,color: Colors.black),
),
onExpansionChanged: (newState) {
setState(() {
selected = findex;
});
},
children: [
ListView.builder(
itemCount: 10,
shrinkWrap: true,
itemBuilder: (BuildContext context, sindex) {
return ListTile(
title: Text(
userList[findex].document[sindex].title,
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold,color: Colors.black),
),
onTap: () {
print(userList[findex].document[sindex].title);
},
);
},
),
],
);
},
),

Make sure ExpansionTile be in stateful widget
ListView.builder(
itemCount: 5,
shrinkWrap: true,
itemBuilder: (BuildContext context, index) {
return CustomExpansionTile(index: index);
},
),
// Expansion Tile Widget
class CustomExpansionTile extends StatefulWidget {
final int index;
const CustomExpansionTile({Key? key, required this.index}) : super(key: key);
#override
State<CustomExpansionTile> createState() => _CustomExpansionTileState();
}
class _CustomExpansionTileState extends State<CustomExpansionTile> {
int selectedIndexExpansionTile = -1;
#override
Widget build(BuildContext context) {
return ExpansionTile(
initiallyExpanded: widget.index == selectedIndexExpansionTile,
key: Key(selectedIndexExpansionTile.toString()),
title: Text(
widget.index.toString(),
),
onExpansionChanged: (newState) {
if (newState) {
selectedIndexExpansionTile = widget.index;
} else {
selectedIndexExpansionTile = -1;
}
setState(() {});
},
children: [Text(widget.index.toString())]);
}
}

Related

All CheckBoxListTile items in a List are being selected instead of just one - Flutter

I'm trying to build a shopping list based on meals ingredients that are passed in Route Settings on which user can select/deselect each items separately. Here is the code:
import 'package:cookup/src/consts/colors.dart';
import 'package:flutter/material.dart';
import '/dummy_data.dart';
class ShoppingListScreen extends StatefulWidget {
static const routeName = '/shoppig-list';
final Function toggleFavorite;
final Function isFavorite;
ShoppingListScreen(this.toggleFavorite, this.isFavorite);
#override
State<ShoppingListScreen> createState() => _ShoppingListScreenState();
}
class _ShoppingListScreenState extends State<ShoppingListScreen> {
bool isSelected = false;
#override
Widget build(BuildContext context) {
final mealId = ModalRoute.of(context)?.settings.arguments as String;
final selectedMeal = DUMMY_MEALS.firstWhere((meal) => meal.id == mealId);
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(color: kFontColorBlack),
backgroundColor: kBackgroundColor,
title: Text(
'${selectedMeal.title}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(color: kFontColorBlack),
),
actions: [
IconButton(
icon: Icon(
widget.isFavorite(mealId)
? Icons.favorite
: Icons.favorite_border,
),
onPressed: () => widget.toggleFavorite(mealId),
),
],
),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 250,
width: double.infinity,
child: Image.network(
selectedMeal.imageUrl,
fit: BoxFit.cover,
),
),
Container(
alignment: Alignment.topLeft,
margin: EdgeInsets.symmetric(vertical: 10),
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Ingredients",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0,
),
),
),
),
ListView.builder(
itemCount: selectedMeal.ingredients.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return CheckboxListTile(
title: Text(selectedMeal.ingredients[index]),
value: isSelected,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (
value,
) {
setState(() {
isSelected = value!;
});
},
);
},
),
],
),
),
);
}
}
and the result is visible here as I got stuck having all items being selcted/deselected on click:
enter image description here
Which is not the expeted behaviour as I need only one item being seletced on click. I was trying variious things but end up having either the above result, having some error regaridng incorrect list Range or I got error that dependOnInheritedWidgetOfExactType<_ModalScopeStatus>() or dependOnInheritedElement() was called before _ShoppingListScreenState.initState() completed. What I am doing wrong? Please help! :)
The reason why you are getting this error is you are passing the same value
isSelected in all your items in the ListView.builder.
You have to add a boolean field in your model class which will store the value for each and every item in your list.
Consider following example for model class.
class DummyMeals {
final String name;
final String id;
bool isSelected;
DummyMeals({
required this.name,
required this.id,
this.isSelected = false,
});
}
ListView.builder(
itemCount: itemList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return CheckboxListTile(
key: Key('$index'),
title: Text(itemList[index].name),
value: itemList[index].isSelected,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (val) {
setState(() {
itemList[index].isSelected = !itemList[index].isSelected;
});
},
);
},
),
Hope this helps !
Thanks! I have updated the model from the list to a map with a boolean value and used below builder code - it works as expected now:
ListView.builder(
itemCount: selectedMeal.ingredients.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final ingredient =
selectedMeal.ingredients.keys.toList()[index];
return CheckboxListTile(
title: Text(ingredient),
key: Key(ingredient),
value: selectedIngredients[ingredient] ?? false,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (
value,
) {
setState(() {
selectedIngredients[ingredient] = value!;
});
},
);
},
),

how to make click with selection on ListTile?

I have US states displayed on the screen. They are displayed using a ListView. I need to make it so that when you click on one of the states, a checkmark appears. Now in the trailing I added an icon, but when you click on one state, a checkmark appears on all. How can this be implemented?
class _AddStatePageState extends State<AddStatePage> {
static const List<String> _usaStates = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
...
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const AppBarWithSearch(
appBarTitle: 'Add State',
),
body: Padding(
padding: const EdgeInsets.only(top: 24),
child: ListView.separated(
itemCount: _usaStates.length,
itemBuilder: (context, index) {
return ListTile(
trailing: Image.asset(
Assets.assetsCheckmark,
width: 13,
height: 10,
),
title: Text(
_usaStates[index],
),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
),
),
);
}
}
Something along these lines:
class _AddStatePageState extends State<AddStatePage> {
static const List<String> _usaStates = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
...
];
static const List<bool> _usaStatesSelected = [false, false, true, ...];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const AppBarWithSearch(
appBarTitle: 'Add State',
),
body: Padding(
padding: const EdgeInsets.only(top: 24),
child: ListView.separated(
itemCount: _usaStates.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
setState(() {
for(var i = 0; i < _usaStatesSelected.length; i++) {
_usaStatesSelected[i] = false;
}
_usaStatesSelected[index] = true;
});
},
trailing:
_usaStatesSelected[index] == false
? SizedBox.shrink()
: Image.asset(
Assets.assetsCheckmark,
width: 13,
height: 10,
),
title: Text(
_usaStates[index],
),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
),
),
);
}
}
ListTile provide onTap method, you can use it. To show selected item, create a variable that will holds the selected index on state class.
int? _selectedIndex;
and ListTile will be
return ListTile(
onTap: () {
_selectedIndex=index;
setState(() {});
},
trailing:
_selectedIndex==index ? Icon(Icons.check) : null,
Replace Icon(Icons.check) with your image.

How to have a tap change only a single item in a ListView?

I'm using a ListView.builder() to display text items from a dynamic list. The text is displayed on Card() widgets. I'd like tapping the text to change its appearance to and from strikeout. I've tried declaring these variables, outside Build():
bool isStrikeout = false;
TextStyle _strikeout =
TextStyle(fontSize: 16, decoration: TextDecoration.lineThrough);
TextStyle _normal = TextStyle(fontSize: 16);
and then, within the Build() method:
body: ListView.builder(
itemCount: someList.length,
itemBuilder: (ctx, index) {
return Card(
elevation: 8,
child: ListTile(
title: InkWell(
onTap: () {
setState(() {
isStrikeout = !isStrikeout;
});
},
child: Container(
padding: const EdgeInsets.all(8),
child: Text(
someList[index].text,
style: isStrikeout ? _strikeout : _normal,
),
// more code here
The problem, of course, is that a user tapping any Card's text will toggle strikeout on and off for all Cards' texts. I only want it to happen for the tapped Card's text.
You need to use List of bool.
Definition
List<bool> strikeList = [];
Initialize
strikeList = List.generate(someList.length, (index)=>false);
Usage
body: ListView.builder(
itemCount: someList.length,
itemBuilder: (ctx, index) {
return Card(
elevation: 8,
child: ListTile(
title: InkWell(
onTap: () {
setState(() {
bool temp = !strikeList[index];
strikeList.removeAt(index);
strikeList.insert(index, temp);
});
},
child: Container(
padding: const EdgeInsets.all(8),
child: Text(
someList[index].text,
style: strikeList[index] ? _strikeout : _normal,
),
// more code here
You try this way
int _selectedIndex = 0;
_onSelected(int index) {
setState(() => _selectedIndex = index);
}
InkWell(
onTap:() => _onSelected(index),
)
Text(
someList[index].text,
style: _selectedIndex == index ? _strikeout : _normal,
),

How to make just one ExpansionPanel, in an ExpansionPanelList different to the others? flutter

As the question suggests I have an ExpansionPanelList, one ExpansionPanel (the last one or the 7th one) should have 2 additional buttons, but how can I add them just in this one last panel & not in all the others as well?
This is the code of my whole Expansion panel, as Im not sure where you have to add the behaviour, but guessing in the body of the ExpansionPanel (close to line 40):
class ExpansionList extends StatefulWidget {
final Info info;
const ExpansionList({
Key key,
this.info,
}) : super(key: key);
#override
_ExpansionListState createState() => _ExpansionListState();
}
class _ExpansionListState extends State<ExpansionList> {
Widget _buildListPanel() {
return Container(
child: Theme(
data: Theme.of(context).copyWith(
cardColor: Color(0xffDDBEA9),
),
child: ExpansionPanelList(
dividerColor: Colors.transparent,
elevation: 0,
expansionCallback: (int index, bool isExpanded) {
setState(() {
infos[index].isExpanded = !isExpanded;
});
},
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: !isExpanded
? Text(
info.headerValue,
) //code if above statement is true
: Text(
info.headerValue,
textScaleFactor: 1.3,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Color(0xffFFE8D6),
borderRadius: BorderRadius.circular(25)),
child: Column(
children: [
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 40.0,),
itemCount: info.expandedValueData.length,
itemBuilder: (context, index) {
return CheckboxListTile(
title: Text(info.expandedValueData[index].title,
style: TextStyle(
decoration: info.expandedValueData[index]
.completed
? TextDecoration.lineThrough
: null)),
value: info.expandedValueData[index].completed,
onChanged: (value) {
setState(() {
// Here you toggle the checked item state
infos.firstWhere(
(currentInfo) => info == currentInfo)
..expandedValueData[index].completed =
value;
});
});
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 20,
);
},
),
Row(children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.16),
Text("Abschnitt bis zum Neustart löschen"),
SizedBox(
width: MediaQuery.of(context).size.width * 0.11),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
infos.removeWhere(
(currentInfo) => info == currentInfo);
});
},
)
]),
],
),
),
),
isExpanded: info.isExpanded);
}).toList(),
),
),
);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _buildListPanel(),
),
);
}
}
Thanks for suggestions!
Hi Just add a field (if you already do not have one) in the info object that will allow you to change the widget that is inflated based on that field.
For example
...
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return info.type == TYPE_A ? TypeAWidgetHeader(info) : TypeBWidgetHeader(info);
body: info.type == TYPE_A ? TypeAWidgetBody(info) : TypeBWidgetBody(info);
...

How do i remove range error running in flutter?

I have created a string List and applied a checkbox and when the checkbox clicked, the string list will be shown on the next screen but I am getting a range error. please help.
var _suggestions = <String>['this is me1','this is me2','this is me3' ];
final _saved = <String>['this is me1','this is me2','this is me3' ];
final _biggerFont = TextStyle(fontSize: 18.0);
this is the string that I have defined.
void _pushSaved(){
Navigator.of(context).push(
MaterialPageRoute<void>(
// NEW lines from here...
builder: (BuildContext context) {
final tiles = _saved.map(
(String pair) {
return ListTile(
title: Text(
pair,
style: _biggerFont,
),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
}, // ...to here.
),
);
}
this is some page route
Widget _buildSuggestions() {
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemBuilder: /*1*/ (context, i) {
if (i.isOdd) return Divider(); /*2*/
final index = i ~/ 2; /*3*/
_suggestions = <String>['this is me1','this is me2','this is me3'];
return _buildRow(_suggestions[index]);
});
}
There is no problem in showing selected checkbox data into the next screen but I don't know why the range error is showing.
Widget _buildRow(String pair) {
final alreadySaved = _saved.contains(pair);
return Container(
decoration: new BoxDecoration (
color: HexColor('#F2FFFF'),
border: Border.all(color: HexColor('#09B9B6')),
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: ListTile(
title: Text(
pair,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.check_box : Icons.check_box_outline_blank_outlined,
color: alreadySaved ? HexColor('#09B9B6') :null,
),
onTap: (){
setState(() {
if (alreadySaved){
_saved.remove(pair);
}
else{
_saved.add(pair);
}
});
},
),
);
}
this is the build row function
We can also use ListView.separated
ListView.separated(
padding: EdgeInsets.all(16.0),
itemBuilder: (context, index) {
return _buildRow(_suggestions[index]);
},
separatorBuilder: (_, __) => Divider(),
itemCount: _suggestions.length);