Recommended approach to lists with expandable elements in Flutter

Viewed 43

I would like to create a list with expandable items/children based on data provided from the stream. Expanded items would include textual details and actions which the user can take.

One of the key requirements for this layout is that - at any time only one list item can be expanded. From what I've managed to found out - with current out-of-the-box Flutter widgets this needs to be managed programmatically. Once user expands one tile to set values as not-expanded for all other tiles and rebuild UI.

I've managed to make this work in two ways:

  1. Using ListView.builder & ExpansionTile widgets as children
  2. Using ExpansionPanelList & ExpansionPanel widgets as children Both approached were combined with Riverpod's StateProvider to keep state of the selected/expanded item and invoke rebuild once the state changes (user taps on a item/tile).

Both approaches have its trade-offs from UI perspective - e.g. ExpansionPanelList children padding is poor looking and cannot be modified. Then we have ExpansionTile approach which looses animation once "key" is set with unique PageStorageKey - a necessary config in order for ExpansionTile to save and restore its expanded state (basically programmatic expanding and collapsing of tiles does not work without this...).

What I don't understand is the performance impact of both approached and which one is better in this regard. How does setting unique key: param and rebuilding ExpansionTile widgets compare ExpansionPanelList and generating list of its items (List<ExpansionPanel>) with each widget rebuild - in order to define which Panel is expanded and which is not.

Intuitively it seems that ListView.builder & ExpansionTile approach is better - but I am not sure what is the impact of setting "key:". Please help in understanding which way is better performance wise. You can see bellow implementation for both approaches.

ListView.builder & ExpansionTile approach

            return ListView.builder(
              padding: EdgeInsets.all(10),
              itemCount: data.length,
              itemBuilder: ((context, index) {
                return ExpansionTile(
                  key: PageStorageKey("${DateTime.now().millisecondsSinceEpoch}"),                  
                  initiallyExpanded: (selectedItem == data[index].id.toString()),
                  onExpansionChanged: ((value) =>
                      ref.read(selectedProductListItem.notifier).state = (value == true) ? data[index].id.toString() : null),
                  tilePadding: EdgeInsets.all(10),
                  leading: Icon(Icons.account_box),
                  title: Text(data[index].name, style: TextStyle(fontWeight: FontWeight.bold)),
                  childrenPadding: EdgeInsets.all(10),
                  expandedAlignment: Alignment.center,
                  children: [
                    Text(data[index].description),
                    SizedBox(height: 10),
                    ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
                  ],
                );
              }),
            );

ExpansionPanelList & ExpansionPanel approach

    final List<ExpansionPanel> listData = [];
    for (var i = 0; i < data.length; i++) {
      listData.add(
        ExpansionPanel(
          headerBuilder: (context, isExpanded) {
            return Padding(padding: EdgeInsets.all(10), child: Text(data[i].name));
          },
          body: Padding(
            padding: EdgeInsets.all(10),
            child: Column(
              children: [
                Text(data[i].description),
                SizedBox(height: 10),
                ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
              ],
            ),
          ),
          isExpanded: (data[i].id == selectedItem),
          canTapOnHeader: true,
        ),
      );
    }
    return SingleChildScrollView(
      padding: EdgeInsets.all(15),
      child: ExpansionPanelList(
        children: listData,
        expansionCallback: (panelIndex, isExpanded) {
          ref.read(selectedProductListItem.notifier).state = isExpanded ? null : data[panelIndex].id;
        },
      ),
    );
0 Answers
Related