expandable_plus 1.2.5
expandable_plus: ^1.2.5 copied to clipboard
Expandable and collapsible panels with accordion groups for Flutter. A maintained, drop-in successor to the expandable package; adds header padding, fixes body-tap toggling.
expandable_plus #
Panels that expand and collapse, cross-fading between two different views rather than clipping one, with accordion groups and a correct screen reader announcement.

Why this instead of what you already have #
Instead of ExpansionTile. Its constructor takes no collapsed-content
parameter (material/expansion_tile.dart:121). Every collapsed* field it
does accept is a style: collapsedBackgroundColor, collapsedTextColor,
collapsedIconColor, collapsedShape. It reveals a single body by animating a
height factor, so there is no cross-fade between two different views, and
nothing coordinates one tile with the next.

That difference decides what a shut panel can say. Growing a height means the
collapsed state is the expanded one with most of it hidden, so a shut row
either shows the top of the form or shows nothing. Two views means a shut
Shipping row can read "Standard, arrives Thursday" and an open one can be the
address form. Redraw the figure with
dart run tool/reveal_modes_figure.dart.
Instead of expandable. Semantics appears nowhere in its source, so
ExpandableButton (lib/expandable.dart:751) hands a screen reader a bare
InkWell with no button role and no expanded state. Four of its open issues
are the ones people hit first: #8 asks for one panel open at a time (April
2019), #50 reports tapBodyToExpand not working (March 2020), #72 asks how
to remove the header padding (September 2020), and #114 reports the example
does not compile (October 2021, filed after the package's last release). The
public API here is the same one, so the move costs an import line.
Reach for it when #
- The collapsed and expanded states show different content, not the same content clipped.
- A set of panels should behave as an accordion with one open at a time.
- Panels need a correct button role and expanded state announced to a screen reader.
Skip it if a Material ExpansionTile already fits your design. It ships with
the framework, it announces its own state changes through a live region
(material/expansion_tile.dart:634), and one fewer dependency is worth more
than the extras here.
Migration from expandable #
Change the import, and your existing panels behave the same.
// before
import 'package:expandable/expandable.dart';
// after
import 'package:expandable_plus/expandable_plus.dart';
The class names, fields, and defaults are the same. Your existing panels look and behave the way they did.
Install #
flutter pub add expandable_plus
Usage #
A basic panel #
ExpandablePanel(
header: const Text('Details'),
collapsed: const Text(
'A short summary.',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
expanded: const Text('The full text goes here.'),
)
An accordion group #
Pass one ExpandableGroupController to several panels. When one opens, the
others close. Panels that are not in a group are unaffected.
final group = ExpandableGroupController();
Column(
children: [
ExpandablePanel(
controller: ExpandableController(group: group),
header: const Text('Section 1'),
collapsed: const Text('Summary 1'),
expanded: const Text('Body 1'),
),
ExpandablePanel(
controller: ExpandableController(group: group),
header: const Text('Section 2'),
collapsed: const Text('Summary 2'),
expanded: const Text('Body 2'),
),
],
)
Pass ExpandableGroupController(allowAllCollapsed: false) to keep one section
open at all times. Dispose the group when you are done with it, for example in
your State.dispose.
Header padding #
headerPadding controls the space around the header. The default is
EdgeInsets.zero, which matches expandable.
ExpandablePanel(
theme: const ExpandableThemeData(headerPadding: EdgeInsets.all(16)),
header: const Text('Details'),
collapsed: const Text('Summary'),
expanded: const Text('Body'),
)
Long lists: lazy #
A cross-fade keeps both children in the tree, and a collapsed panel still builds
its expanded body. One panel never notices. Twenty do: put twenty collapsed
ExpandablePanels in a ListView and fifteen expanded bodies are built on the
first frame, one for every panel the viewport lays out. Measured in
test/lazy_test.dart, which pins the number. Redraw the figure with
dart run tool/lazy_cost_figure.dart.

lazy: true holds a panel's body back until it first opens:
ExpandablePanel(
lazy: true,
header: const Text('Section'),
collapsed: const Text('Summary'),
// Your widget. `lazy` is what keeps it unbuilt until the panel opens,
// which is the whole reason to reach for it.
expanded: const HeavyBody(),
)
The first expand swaps the real child in, and it stays. Closing and reopening
costs nothing and keeps whatever state the body was holding, because its
initState runs once.
Off by default. Turning it on moves when a child's initState runs. That
matters if the body has to be alive before anyone opens it, which makes this a
decision rather than a default. Panels built through builder place both
children themselves and are unaffected.
Accessibility #
ExpandableButton wraps the header in Semantics(button: true, expanded: ...).
There is no label or semantics parameter to pass. The header widget is the
name: a Text('Shipping') header is announced as Shipping.
example/test/screen_reader_transcript_test.dart drives the example accordion
and asserts those flags after every tap, then writes them in this spoken form.
It is the node's label, the button role, and the expanded flag — not a
recording of VoiceOver. First frame, Shipping already open:
Shipping, button, expanded
Payment, button, collapsed
Returns, button, collapsed
Tap Payment. The group closes Shipping; nobody tapped it:
Shipping, button, collapsed
Payment, button, expanded
Returns, button, collapsed
Tap Payment again:
Shipping, button, collapsed
Payment, button, collapsed
Returns, button, collapsed
The same three titles built as a bare InkWell, which is what expandable
hands a screen reader (Semantics appears nowhere in its source):
Shipping tappable, no button role, no open state
Payment tappable, no button role, no open state
Returns tappable, no button role, no open state
ExpandablePanel does this for you. A custom layout still has to wrap the
subtree in ExpandableNotifier and put the header in ExpandableButton;
otherwise the node is missing. Leave tapHeaderToExpand at its default
(true) so the header text sits inside the button. Turn it off and only the
chevron is the button, and it has no name.
The flag follows the controller, so expanding from code updates the announcement too. The change is the flag on the button, not a live region: a screen reader focused on the header hears the new state.
What's fixed #
- Open one panel at a time: #8
- Remove the padding around the header: #72
tapBodyToExpandandtapBodyToCollapsenot working: #50- Example not compiling: #114
- No screen-reader support: the header exposed no button role and no expanded state, leaving the control unusable with assistive technology
Credits #
Based on expandable by Alexander Ryzhov (MIT). Original repository:
https://github.com/aryzhov/flutter-expandable
License #
MIT. See LICENSE.
