Close an ExpansionTile when another ExpansionTile is tapped

8.9k views Asked by At

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);
                            },
                          );
                        },
                      ),
                    ],
                  );
                },
              ),
7

There are 7 answers

0
Paras Arora On

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())]);
              }
            }
3
Bharath On

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

2
Xuuan Thuc On

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);
                      },
                    );
                  },
                ),
              ],
            );
          },
        ),
2
Ravindra S. Patil On

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;
          });
      }),
    );
  },
);
0
Reinier Garcia On

Simple Alternative using ExpansionTileControllers

For example, if you have two ExpansionTile widgets:

First you need to declare two properties ExpansionTileController in the state class.

And then you must assign both variables to the respective ExpansionTile widget in your build method.

  final expansionTileControllerTheme = ExpansionTileController();
  final expansionTileControllerLanguage = ExpansionTileController();
.
.
.
        // App Theme Color selector
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          child: ExpansionTile(
            controller: expansionTileControllerTheme,
            onExpansionChanged: (bool isOpen) {
              if (isOpen) expansionTileControllerLanguage.collapse();
            },
            iconColor: selectedColor,
            collapsedIconColor: selectedColor,
            title: const Row(
              children: [Icon(FontAwesomeIcons.palette), SizedBox(width: 10), Text('Theme Color')],
            ),Text('Active: ${selectedColor.colorName}')]),
            children: [
              ...colorList.asMap().entries.map(
                (entry) {
                  final int index = entry.key;
                  final Color color = entry.value;

                  return RadioListTile(
                    title: Text(color.colorName, style: TextStyle(color: color)),
                    value: index,
                    groupValue: appTheme.colorIndex,
                    onChanged: (_) => changeColorIndex(index),
                  );
                },
              )
            ],
          ),
        ),

        // Language Selector
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          child: ExpansionTile(
            controller: expansionTileControllerLanguage,
            onExpansionChanged: (bool isOpen) {
              if (isOpen) expansionTileControllerTheme.collapse();
            },
            iconColor: selectedColor,
            collapsedIconColor: selectedColor,
            title: const Row(
              children: [Icon(FontAwesomeIcons.language), SizedBox(width: 12), Text('Language')],
            ),
            children: [
              ...LanguageEntity.languageList().map(
                (LanguageEntity language) {
                  return RadioListTile(
                    title: Row(
                      children: [
                        Text(language.flagEmoji, style: TextStyle(color: selectedColor)),
                        const SizedBox(width: 8),
                        Text(language.name, style: TextStyle(color: selectedColor)),
                      ],
                    ),
                    value: language.id,
                    groupValue: 2,
                    onChanged: (_) => {},
                  );
                },
              )
            ],
          ),
        ),
.
.
.

When one ExpansionTile is open, which you can detect in the event onExpansionChanged, you simply send to collapse the other one.

Visual Result:

enter image description here

0
Sourav Suman On

Use Accordion Wizard package, if you don't need to use ListView.builder for creating expansion tiles.

1
Sepehr Marashi On
key: index == lastOne ? const Key("selected") : Key(index.toString()),
                                                initiallyExpanded: index == selectedIndex,
                                                expandedCrossAxisAlignment: CrossAxisAlignment.start,
                                                onExpansionChanged: ((newState) {
                                                  lastOne = selectedIndex;
                                                  if (newState) {
                                                    setState(() {
                                                      selectedIndex = index;
                                                    });
                                                  } else {
                                                    setState(() {
                                                      selectedIndex = -1;
                                                    });
                                                  }
                                                }),

in this way you can also have the animation as well