Flutter - Using ExpansionPanelList Widget Examples

This tutorial shows you how to use ExpansionPanelList in Flutter, including how to create ExpansionPanel as its children and handle expanded/collapsed state. Other things in this article include how to change the divider color, set animation duration, and set the padding when the header is expanded.

Sometimes, you may need to create a list view where the items can have collapsible additional details. In Flutter, there is a widget that allows you to create such a list. The widget is ExpansionPanelList, which is a material expansion panel list. It contains a list of items as its children, with each item can be expanded and collapsed. In addition, it also plays animation when the state of a list item changes from collapsed to expanded or vice versa. Below are the explanation and examples of how to use the widget.

Using ExpansionPanelList Widget

Here is the constructor of ExpansionPanelList.

  const ExpansionPanelList({
    Key? key,
    List<ExpansionPanel> children = const <ExpansionPanel>[],
    ExpansionPanelCallback? expansionCallback,
    Duration animationDuration = kThemeAnimationDuration,
    EdgeInsets expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
    Color? dividerColor,
    int elevation = 2,
  })

There is no required argument. However, the children and expansionCallback arguments are usually passed in most cases. For the children argument, you need to pass a list of ExpansionPanel items. Meanwhile, the expansionCallback argument is important for handling the state of each item.

The widget cannot be placed anywhere in the tree since it uses RenderListBody for the children. RenderListBody requires unlimited space along its main axis. In other words, the main axis should not be constrained. Below is the assertion error if you do not fulfil the requirement.

  ======== Exception caught by rendering library =====================================================
  The following assertion was thrown during performLayout():
RenderListBody must have unlimited space along its main axis.

  RenderListBody does not clip or resize its children, so it must be placed in a parent that does not constrain the main axis.

  You probably want to put the RenderListBody inside a RenderViewport with a matching main axis.

The easiest solution is to put the ExpansionPanelList as the child of SingleChildScrollView, which allows a single widget to be scrolled if the container is too small.

  SingleChildScrollView(
    child: ExpansionPanelList(
      // put the arguments here
    )
  )

In this tutorial, we are going to create a list using the below class as the data structure.

  class Item {
    Item({
      this.id,
      this.name,
      this.description,
      this.isExpanded = false,
    });
  
    int id;
    String name;
    String description;
    bool isExpanded;
  }

id is a unique identifier. The name property is used for the title, while the collapsible content is stored in the description property. The state whether an item is in expanded or collapsed state is stored in a property called isExpanded. Below is the code for populating the list and storing the data in a state variable _items.

  List<Item> _items;

  @override
  void initState() {
    super.initState();
    setState(() {
      _items = _generateItems();
    });
  }

  List<Item> _generateItems() {
    return List.generate(20, (int index) {
      return Item(
        id: index,
        name: 'Item $index',
        description: 'Details of item $index',
      );
    });
  }

Create ExpansionPanel

An ExpansionPanelList usually have some ExpansionPanels as its children. Therefore, you need to know how to build an ExpansionPanel using the constructor below.

  ExpansionPanel({
    required ExpansionPanelHeaderBuilder headerBuilder,
    required Widget body,
    bool isExpanded = false,
    bool canTapOnHeader = false,
    Color backgroundColor,
  })

The constructor requires you to pass two named arguments: headerBuilder and body. For the headerBuilder, you need to pass a ExpansionPanelHeaderBuilder function. The passed function needs to accept two parameters whose types in order are BuildContext and isExpanded. The return type is a Widget which will be set as the header of the ExpansionPanel.

  Widget Function(BuildContext context, bool isExpanded)

The other required argument body is the widget to be displayed below the header in expanded state.

Apart from those two required arguments, another essential argument that you need to pass is isExpanded. It's used to control whether the current state is collapsed or expanded.

The method below is used to create an ExpansionPanel given an Item instance. It uses ListTile, which is very common to be used as a list item, as the return type of the headerBuilder function as well as the body. It also passes the isExpanded argument, making it possible to dynamically set whether the ExpansionPanel is expanded or not.

  ExpansionPanel _buildExpansionPanel(Item item) {
    return ExpansionPanel(
      isExpanded: item.isExpanded,
      headerBuilder: (BuildContext context, bool isExpanded) {
        return ListTile(
          title: Text(item.name),
          subtitle: Text(item.name),
        );
      },
      body: ListTile(
        title: Text(item.description),
      ),
    );
  }

Then, we can map the _items data to ExpansionPanel as you can see on the code below.

  ExpansionPanelList(
    children: _items.map((item) => _buildExpansionPanel(item))
        .toList(),
    // other arguments
  )

Allow Tap on Header

By default, the users can only tap on the expand/collapse icon in order to make a list item expanded or collapsed. If you also want to make a tap on the header does the same thing, you can pass isExpanded argument with true as the value.

  ExpansionPanel _buildExpansionPanel(Item item) {
    return ExpansionPanel(
      canTapOnHeader: true,
      // other arguments
    );
  }

Set Background Color

The background color of an ExpansionPanel can be set by passing backgroundColor argument whose type is Color. It affects the entire area, including the header, body, and the area surrounding the expand/collapse icon.

  ExpansionPanel _buildExpansionPanel(Item item) {
    return ExpansionPanel(
      backgroundColor: Colors.lightBlueAccent,
      // other arguments
    );
  }

Output:

Flutter - ExpansionPanel - Background Color

Create ExpansionPanelCallback

When a user presses the expand/collapse icon, the state of the panel should change. Since the state of a panel depends on the value passed as isExpanded argument, you also need to change the value of the variable passed as isExpanded. In order to know if a user presses the expand/collapse icon, you need to pass a callback function as expansionCallback. The passed function must have two parameters whose types in order are int and bool. The first parameter is the index of the panel in the list, while the second parameter is the expanded state. The code below changes the isExpanded property of an item at the given index..

  ExpansionPanelList(
    expansionCallback: (int index, bool isExpanded) {
      setState(() {
        _items[index].isExpanded = !isExpanded;
      });
    },
    // other arguments
  )

Set Animation Duration

When the state of a panel changes from collapsed to expanded or vice versa, there will be an animation played for a certain amount of time. You can change the duration of the animation by passing a Duration value as the animationDuration argument.

  ExpansionPanelList(
    animationDuration: const Duration(seconds: 2),
    // other arguments
  )

Set Expanded Header Padding

When the panel is in expanded state, you can set the padding for the header by passing expandedHeaderPadding. The argument type is EdgeInsets, which is the same as the type used on Padding widget.

  ExpansionPanelList(
    expandedHeaderPadding: EdgeInsets.all(10),
    // other arguments
  )

Output:

Flutter - ExpansionPanelList - Expanded Header Padding

Set Divider Color

Each item in the list is separated by a divider. The color of the divider can be set by passing a Color value as dividerColor argument.

  ExpansionPanelList(
    dividerColor: Colors.red,
    // other arguments
  )
Flutter - ExpansionPanelList - Divider Color

Set Elevation

When a panel is expanded, you can set the elevation by passing the elevation argument. Despite the type is double, you can only pass certain values as defined in kElevationToShadow.

  ExpansionPanelList(
    elevation: 4,
    // other arguments
  )

Putting the above code together, below is an example of how to use the ExpansionPanelList widget.

  ExpansionPanel _buildExpansionPanel(Item item) {
    return ExpansionPanel(
      isExpanded: item.isExpanded,
      backgroundColor: Colors.lightBlueAccent,
      canTapOnHeader: true,
      headerBuilder: (BuildContext context, bool isExpanded) {
        return Container(
          child: ListTile(
            title: Text(item.name),
            subtitle: Text(item.name),
          ),
        );
      },
      body: ListTile(
        title: Text(item.description),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Woolha.com Flutter Tutorial"),
      ),
      body: SingleChildScrollView(
        child: ExpansionPanelList(
          animationDuration: const Duration(seconds: 2),
          expandedHeaderPadding: EdgeInsets.all(10),
          dividerColor: Colors.red,
          elevation: 4,
          expansionCallback: (int index, bool isExpanded) {
            setState(() {
              _items[index].isExpanded = !isExpanded;
            });
          },
          children: _items.map((item) => _buildExpansionPanel(item))
              .toList(),
        ),
      ),
    );
  }

Output:

Flutter - ExpansionPanelList

Using ExpansionPanelList.radio

What if you want to allow only one ExpansionPanel in expanded state at the same time? It's possible to change the logic inside the function passed as expansionCallback. However, there is an easier way to do that. You can use ExpansionPanelList.radio named constructor.

  const ExpansionPanelList.radio({
    Key? key,
    List<ExpansionPanelRadio> children = const <ExpansionPanelRadio>[],
    ExpansionPanelCallback? expansionCallback,
    Duration animationDuration = kThemeAnimationDuration,
    Object? initialOpenPanelValue
    EdgeInsets expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
    Color? dividerColor,
    int elevation = 2,
  })

The parameters of the named constructor are very similar to the ExpansionPanel's constructor, except the type of the children's element is ExpansionPanelRadio. It also has an additional argument initialOpenPanelValue which is used to set which panel should be opened initially.

Create ExpansionPanelRadio

Below is the constructor of ExpansionPanelRadio.

  ExpansionPanelRadio({
    required Object value,
    required ExpansionPanelHeaderBuilder headerBuilder,
    required Widget body,
    bool canTapOnHeader = false,
    Color? backgroundColor,
  })

Using ExpansionPanelRadio is similar to using ExpansionPanel. One of the differences is you are required to pass an Object as the value argument. It's used to identify which radio panel is currently selected. The other difference is it doesn't have isExpanded parameter, since it becomes Flutter's responsibility to handle which panel should be opened.

Below is an example of how to use ExpansionPanelList.Radio.

  ExpansionPanelRadio _buildExpansionPanelRadio(Item item) {
    return ExpansionPanelRadio(
      value: item.id,
      backgroundColor: Colors.lightBlueAccent,
      canTapOnHeader: true,
      headerBuilder: (BuildContext context, bool isExpanded) {
        return Container(
          child: ListTile(
            title: Text(item.name),
            subtitle: Text(item.name),
          ),
        );
      },
      body: ListTile(
        title: Text(item.description),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("Woolha.com Flutter Tutorial"),
        ),
        body: SingleChildScrollView(
          child: ExpansionPanelList.radio(
            animationDuration: const Duration(seconds: 2),
            expandedHeaderPadding: EdgeInsets.all(10),
            dividerColor: Colors.red,
            elevation: 4,
            initialOpenPanelValue: 3,
            expansionCallback: (int index, bool isExpanded) {
              setState(() {
                _items[index].isExpanded = !isExpanded;
              });
            },
            children: _items.map((item) => _buildExpansionPanelRadio(item))
                .toList(),
          ),
        )
    );
  }

Output:

Flutter - ExpansionPanelList.radio

Full Code

  import 'package:flutter/material.dart';
  
  void main() => runApp(MyApp());
  
  class MyApp extends StatelessWidget {
  
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Woolha.com Flutter Tutorial',
        home: ExpansionPanelListExample(),
        debugShowCheckedModeBanner: false,
      );
    }
  }
  
  class Item {
    Item({
      this.id,
      this.name,
      this.description,
      this.isExpanded = false,
    });
  
    int id;
    String name;
    String description;
    bool isExpanded;
  }
  
  class ExpansionPanelListExample extends StatefulWidget {
  
    @override
    State<StatefulWidget> createState() {
      return ExpansionPanelListExampleState();
    }
  }
  
  class ExpansionPanelListExampleState extends State<ExpansionPanelListExample> {
  
    List<Item> _items;
  
    @override
    void initState() {
      super.initState();
      setState(() {
        _items = _generateItems();
      });
    }
  
    List<Item> _generateItems() {
      return List.generate(20, (int index) {
        return Item(
          id: index,
          name: 'Item $index',
          description: 'Details of item $index',
        );
      });
    }
  
    ExpansionPanel _buildExpansionPanel(Item item) {
      return ExpansionPanel(
        isExpanded: item.isExpanded,
        backgroundColor: Colors.lightBlueAccent,
        canTapOnHeader: true,
        headerBuilder: (BuildContext context, bool isExpanded) {
          return Container(
            child: ListTile(
              title: Text(item.name),
              subtitle: Text(item.name),
            ),
          );
        },
        body: ListTile(
          title: Text(item.description),
        ),
      );
    }
  
    ExpansionPanelRadio _buildExpansionPanelRadio(Item item) {
      return ExpansionPanelRadio(
        value: item.id,
        backgroundColor: Colors.lightBlueAccent,
        canTapOnHeader: true,
        headerBuilder: (BuildContext context, bool isExpanded) {
          return Container(
            child: ListTile(
              title: Text(item.name),
              subtitle: Text(item.name),
            ),
          );
        },
        body: ListTile(
          title: Text(item.description),
        ),
      );
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
          appBar: AppBar(
            title: const Text("Woolha.com Flutter Tutorial"),
          ),
          body: SingleChildScrollView(
            child: ExpansionPanelList(
              animationDuration: const Duration(seconds: 2),
              expandedHeaderPadding: EdgeInsets.all(10),
              dividerColor: Colors.red,
              elevation: 4,
              expansionCallback: (int index, bool isExpanded) {
                setState(() {
                  _items[index].isExpanded = !isExpanded;
                });
              },
              children: _items.map((item) => _buildExpansionPanel(item))
                  .toList(),
            ),
          ),
          // body: SingleChildScrollView(
          //   child: ExpansionPanelList.radio(
          //     animationDuration: const Duration(seconds: 2),
          //     expandedHeaderPadding: EdgeInsets.all(10),
          //     dividerColor: Colors.red,
          //     elevation: 4,
          //     initialOpenPanelValue: 3,
          //     expansionCallback: (int index, bool isExpanded) {
          //       setState(() {
          //         _items[index].isExpanded = !isExpanded;
          //       });
          //     },
          //     children: _items.map((item) => _buildExpansionPanelRadio(item))
          //         .toList(),
          //   ),
          // ),
      );
    }
  }
  

ExpansionPanelList - Parameters

  • Key? key: The widget's key, used to control how a widget is replaced with another widget.
  • List<ExpansionPanel> children: The children of the expansion panel list. Defaults to const <ExpansionPanel>[].
  • ExpansionPanelCallback? expansionCallback: A callback that will be called whenever an expand/collapse button is pressed.
  • Duration animationDuration: The duration of the animation. Defaults to kThemeAnimationDuration.
  • EdgeInsets expandedHeaderPadding: The padding that surrounds the panel header when expanded. Defaults to _kPanelHeaderExpandedDefaultPadding.
  • Color? dividerColor: Color for the divider.
  • int elevation: The elevation for the ExpansionPanel while it's expanded. Defaults to 2.

?: value can be null.

ExpansionPanel - Parameters

  • required ExpansionPanelHeaderBuilder headerBuilder: A widget builder for building the header.
  • required Widget body: The body which is displayed below the header.
  • bool isExpanded: Whether the panel is expanded.
  • bool canTapOnHeader: Whether tapping on the panel's header will expand/collapse it.
  • Color backgroundColor: Defines the background color of the panel.

?: value can be null.
required: value must be passed.

ExpansionPanelList.radio - Parameters

  • Key? key: The widget's key, used to control how a widget is replaced with another widget.
  • List<ExpansionPanelRadio> children: The children of the expansion panel list. Defaults to const <ExpansionPanelRadio>[].
  • ExpansionPanelCallback? expansionCallback: A callback that will be called whenever an expand/collapse button is pressed.
  • Duration animationDuration: The duration of the animation. Defaults to kThemeAnimationDuration.
  • Object? initialOpenPanelValue: A value that indicates which panel should be opened initially.
  • EdgeInsets expandedHeaderPadding: The padding that surrounds the panel header when expanded. Defaults to _kPanelHeaderExpandedDefaultPadding.
  • Color? dividerColor: Color for the divider.
  • int elevation: The elevation for the ExpansionPanel while it's expanded. Defaults to 2.

?: value can be null.

ExpansionPanelRadio - Parameters

  • required Object value: The identifier for the panel.
  • required ExpansionPanelHeaderBuilder headerBuilder: A widget builder for building the header.
  • required Widget body: The body which is displayed below the header.
  • bool canTapOnHeader: Whether tapping on the panel's header will expand/collapse it.
  • Color backgroundColor: Defines the background color of the panel.

?: value can be null.
required: value must be passed.

Summary

This tutorial explains how to use ExpansionPanelList in Flutter. Basically, you need to define a list of ExpansionPanel instances which will be rendered as the children. It's also necessary to correctly handle the expanded state of each item by implementing correct logic inside the function passed as expansionCallback. You can also customize a few things such as the elevation of the expanded items, the animation duration, and the divider color. If you only allow one panel in expanded state at the same time, you can use ExpansionPanelList.radio.