expandable_panel 0.1.0 copy "expandable_panel: ^0.1.0" to clipboard
expandable_panel: ^0.1.0 copied to clipboard

Expandable and collapsible panels for Flutter with accordion groups, accessibility, keyboard and RTL support, reduced motion and state restoration.

expandable_panel #

Expandable and collapsible panels for Flutter, with accordion groups, screen-reader and keyboard support, RTL layouts, reduced motion and state restoration. The API mirrors the expandable package closely, so most apps can migrate by changing one import.

pub package License: MIT

Features #

  • Familiar API. It has the same widgets as expandable: ExpandablePanel, Expandable, ExpandableNotifier, ExpandableController, ExpandableButton, ExpandableIcon, ScrollOnExpand, ExpandableTheme and ExpandableThemeData, with the same theme fields.
  • Accordion. ExpandableGroup keeps at most one panel open, or at most maxExpanded panels, or leaves the count unlimited. Its state object offers collapseAll().
  • Accessibility. The header is one button in the semantics tree. It reports an expanded or collapsed state and gives the localized hint "Expand" or "Collapse". Hidden content is left out of the semantics tree and of focus traversal.
  • Keyboard. The header can take focus and shows a focus highlight. Enter, numpad Enter and Space toggle it.
  • RTL. The icon is placed at start or end by default, so it mirrors in right-to-left locales. Use left or right for a fixed physical side.
  • Reduced motion. When MediaQuery.disableAnimations is set, panels, icons and scrolls snap without animating.
  • Initial state and restoration. Use initialExpanded, or set restorationId to save and restore the state with RestorationMixin.
  • Lazy content. The expanded child is not built until the panel first expands. expandedBuilder defers even the construction of the widget.
  • Keep state. With maintainState: true, the expanded content keeps its state while collapsed. While collapsed it is offstage, its tickers are muted, and it cannot receive focus.
  • Material 3 defaults. The icon color comes from ColorScheme.onSurfaceVariant, so it follows dark mode. The motion uses Durations.medium2 and Easing.standard.

Fixed issues from expandable #

Each fix is covered by a widget test. Rows marked R were also reproduced by running the same test against expandable 5.0.1, where it fails. The other rows are fixes for causes visible in the 5.0.1 source.

Problem Upstream issue What changed
R ScrollOnExpand scrolls to the wrong place when animationDuration differs from scrollAnimationDuration #51 The scroll waits until every Expandable below it has finished animating, instead of starting on a timer.
R scrollOnExpand: false still scrolls #130 The flag is honoured.
R Inside a scrollable nested in another scrollable, the expanded panel ends up clipped no open issue The scroll runs on the final layout and reveals the panel in every enclosing viewport.
R The icon stops rotating, or the panel stops following the controller, after a parent rebuilds with a new controller #69, #95, #117 Every widget rebinds when the controller in scope is replaced.
R Content that changes height while expanded animates again (jank), and nested panels lag behind their parent related: #108, #110 Once settled, the body takes its content's size directly. During the animation, the size is interpolated from both children's live sizes on every frame.
Nested panels open and close their parents #68 A panel nested in another panel's body has its own state.
The icon stays dark in a dark theme #122 The icon color follows the color scheme.
ExpandableThemeData(iconPadding: EdgeInsets.zero) is treated as empty #66 Equality compares every field.
A crossFadePoint outside 0 to 1 fails with a cryptic curve assertion #86 The value is checked in the constructor.

Install #

dependencies:
  expandable_panel: ^0.1.0
import 'package:expandable_panel/expandable_panel.dart';

Usage #

A panel #

class ArticleTile extends StatelessWidget {
  const ArticleTile({super.key, required this.title, required this.body});

  final String title;
  final String body;

  @override
  Widget build(BuildContext context) {
    return ExpandablePanel(
      header: Text(title),
      collapsed: Text(body, maxLines: 2, overflow: TextOverflow.ellipsis),
      expanded: Text(body),
    );
  }
}

Cards in a list that scroll into view #

Widget articleList(List<String> bodies) {
  return ListView.builder(
    itemCount: bodies.length,
    itemBuilder: (context, i) => ExpandableNotifier(
      key: ValueKey(i),
      restorationId: 'article-$i',
      child: ScrollOnExpand(
        child: Card(
          child: ExpandablePanel(
            theme: const ExpandableThemeData(tapBodyToCollapse: true),
            header: Text('Article ${i + 1}'),
            collapsed: Text(bodies[i], maxLines: 2),
            expanded: Text(bodies[i]),
          ),
        ),
      ),
    ),
  );
}

Accordion #

Widget faq(Map<String, String> answers) {
  return ExpandableGroup(
    maxExpanded: 1, // at most one open; use 2, 3 ... or null for no limit
    child: Column(
      children: [
        for (final entry in answers.entries)
          ExpandablePanel(
            header: ListTile(title: Text(entry.key)),
            expanded: Text(entry.value),
          ),
      ],
    ),
  );
}

Programmatic control #

final controller = ExpandableController(initialExpanded: true);

Widget details() {
  return ExpandablePanel(
    controller: controller,
    header: const Text('Details'),
    expanded: const Text('More details'),
  );
}

void examples() {
  controller.toggle();
  controller.expanded = false;
  controller.addListener(() => debugPrint('expanded: ${controller.value}'));
}

Custom layouts #

Widget photos() {
  return ExpandableNotifier(
    child: Expandable(
      collapsed: const ExpandableButton(child: Text('Show all photos')),
      expanded: Column(
        children: [
          for (var i = 1; i <= 6; i++) Text('Photo $i'),
          const ExpandableButton(child: Text('Back')),
        ],
      ),
    ),
  );
}

Lazy and kept-alive content #

Widget comments() {
  return ExpandablePanel(
    header: const Text('Comments'),
    // Built only when the panel opens.
    expandedBuilder: (context) => ListView(
      shrinkWrap: true,
      children: const [TextField()],
    ),
    // Keep the text field's content while collapsed.
    maintainState: true,
  );
}

The example app in example/ shows all of these.

Migrating from expandable #

  1. Replace the dependency and the import:

    dependencies:
      expandable_panel: ^0.1.0   # was: expandable: ^5.0.1
    
    import 'package:expandable_panel/expandable_panel.dart';
    // was: import 'package:expandable/expandable.dart';
    
  2. Class names, constructor parameters and theme fields are unchanged: ExpandablePanel(header, collapsed, expanded, controller, builder, theme), Expandable(collapsed, expanded, controller, theme), ExpandableNotifier(controller, initialExpanded, child), ExpandableController(initialExpanded) with expanded, value, toggle() and of(context, rebuildOnChange:, required:), ExpandableButton, ExpandableIcon, ScrollOnExpand(scrollOnExpand, scrollOnCollapse, theme), ExpandableTheme and ExpandableThemeData with combine, of, withDefaults, defaults and empty.

  3. Check these behaviour changes:

    Area expandable 5.0.1 expandable_panel
    Icon color Colors.black54 ColorScheme.onSurfaceVariant. Set iconColor to keep the old color.
    Header alignment top center. Set headerAlignment: ExpandablePanelHeaderAlignment.top for the old layout.
    Icon placement right end, which is still the right side in LTR. Use right to keep it on the right in RTL too.
    Body and child alignment left / Alignment.topLeft start / AlignmentDirectional.topStart
    Size curve Curves.fastOutSlowIn, 300 ms Easing.standard, 300 ms (Durations.medium2)
    iconPadding type EdgeInsets EdgeInsetsGeometry. Passing an EdgeInsets still works.
    collapsed / expanded both required collapsed defaults to an empty box. Pass either expanded or expandedBuilder.
    Expanded child built immediately, kept in the tree built on first expansion and removed after collapsing. Use maintainState: true if it must keep state or a GlobalKey.
    Nested ExpandablePanel without a controller shared the outer panel's controller gets its own state. Pass the same controller if you want them linked.
    Missing controller debug assertion; release builds show the expanded child a FlutterError naming ExpandableNotifier
    ExpandableThemeData.isFull() required iconColor ignores iconColor, which is resolved from the color scheme
    collapsedFadeStart and similar getters public removed
  4. The new options are opt-in: ExpandableGroup, initialExpanded and restorationId on ExpandablePanel, maintainState, expandedBuilder, ExpandableController.expand() / collapse(), ExpandableThemeData.copyWith / merge, and the start / end placement values.

Limitations #

  • Panels expand vertically only. Horizontal expansion (#80, #98) is not supported.
  • crossFadePoint applies symmetrically: when collapsing, the timeline runs backwards. Separate expand and collapse points (#23) are not supported.
  • There is no onExpansionChanged callback. Listen to the ExpandableController, which is a ValueListenable<bool>.
  • ScrollOnExpand waits for Expandable widgets below it. Any other animation you run inside is not waited for. The widget keeps its list item alive (AutomaticKeepAlive) until the scroll has run, which needs the list's default addAutomaticKeepAlives: true.
  • An ExpandableGroup counts only the panels and notifiers directly inside it. Panels nested inside a member are independent.
  • Semantics, focus and keyboard behaviour are verified with widget tests in the semantics tree. They have not been tried with TalkBack or VoiceOver on a device.
  • ExpandableButton with the default useInkWell: true needs a Material ancestor, as InkWell does.

License #

MIT. See LICENSE.

This is a clean-room implementation written against the public API and the issue tracker of expandable (MIT, Alexander Ryzhov). It does not copy that package's code.

Made by Abdeldjalil Chougui.

0
likes
160
points
331
downloads

Documentation

API reference

Publisher

verified publisherabdeldjalilchougui.biz

Weekly Downloads

Expandable and collapsible panels for Flutter with accordion groups, accessibility, keyboard and RTL support, reduced motion and state restoration.

Homepage
Repository (GitHub)
View/report issues

Topics

#expandable #accordion #collapsible #widget #accessibility

License

MIT (license)

Dependencies

flutter

More

Packages that depend on expandable_panel