expandable 4.1.4

  • Readme
  • Changelog
  • Example
  • Installing
  • 96

Expandable #

A Flutter widget that can be expanded or collapsed by the user.

Introduction #

This library helps implement expandable behavior as prescribed by Material Design:

animated image

Expandable should not be confused with ExpansionPanel. ExpansionPanel, which is a part of Flutter material library, is designed to work only within ExpansionPanelList and cannot be used for making other widgets, for example, expandable Card widgets.

Usage #

The easiest way to make an expandable widget is to use ExpandablePanel:

class ArticleWidget extends StatelessWidget {
  
  final Article article;
  
  ArticleWidget(this.article);

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

ExpandablePanel has a number of properties to customize its behavior, but it's restricted by having a title at the top and an expand icon shown as a down arrow (on the right or on the left). If that's not enough, you can implement custom expandable widgets by using a combination of Expandable, ExpandableNotifier, and ExpandableButton:

class EventPhotos extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ExpandableNotifier(  // <-- Provides ExpandableController to its children
      child: Column(
        children: [
          Expandable(           // <-- Driven by ExpandableController from ExpandableNotifier
            collapsed: ExpandableButton(  // <-- Expands when tapped on the cover photo
              child: buildCoverPhoto(),
            ),
            expanded: Column(  
              children: [
                buildAllPhotos(),
                ExpandableButton(       // <-- Collapses when tapped on
                  child: Text("Back"),
                ),
              ]
            ),
          ),
        ],
      ),
    );
  }
}

Automatic Scrolling #

Expandable widgets are often used within a scroll view. When the user expands a widget, be it an ExpandablePanel or an Expandable with a custom control, they expect the expanded widget to fit within the viewable area (if possible). For example, if you show a list of articles with a summary of each article, and the user expands an article to read it, they expect the expanded article to occupy as much screen space as possible. The Expandable package contains a widget to help implement this behavior, ScrollOnExpand. Here's how to use it:

   ExpandableNotifier(      // <-- This is where your controller lives
     //...
     ScrollOnExpand(        // <-- Wraps the widget to scroll
      //...
        ExpandablePanel(    // <-- Your Expandable or ExpandablePanel
          //...
        )
     )
  )

Why a separate widget, you might ask? Because generally you might want to show not just the expanded widget but its container, for example a Card that contains it. See the example app for more details on the usage of ScrollOnExpand.

Themes #

Version 4.0 introduced themes to expandable widgets. Themes greatly simplify visual customization and as well as future extensibility. Each expandable widget can get its theme data directly via the constructor or from the nearest ExpandableTheme widget:

  ExpandableTheme(
    data: ExpandableThemeData(
        iconColor: Colors.red, 
        animationDuration: const Duration(milliseconds: 500)
    ),
      // ...
      Expandable(  // 500 milliseconds animation duration
      ),

      // ...
      ExpandablePanel(  // <-- Blue icon color, 500 milliseconds animation duration
        theme: ExpandableThemeData(iconColor: Colors.blue),
  
      ),
  )

ExpandableTheme widgets can be nested, in which case inner definitions override outer definitions. There is a default theme with fallback values defined in ExpandableThemeData.defaults.

Prior to version 4.0, theme parameters were passed to widget constructors directly. These parameters are now deprecated and will be removed in the next release.

Icon Customization #

There are several theme properties that help customize the expand/collapse icon:

  • hasIcon - should the icon be shown in the header of [ExpandablePanel];
  • iconSize - icon size;
  • iconColor - icon color;
  • iconPadding - icon padding;
  • iconPlacement - icon placement in the header;
  • iconRotationAngle - the angle to which to rotate the icon;
  • expandIcon - icon face to use in the collapsed state;
  • collapseIcon - icon face to use in the expanded state.

When specifying a custom icon, you have the option to use the same icon for expand/collapse or to use two different icons. If using the same icon, specify the same value for expandIcon and collapseIcon, and that icon will be shown as-is in the collapsed state and upside-down in the expanded state.

Migration #

If you have migration issues from a previous version, read the Migration Guide.

[4.1.4] #

  • Added inkWellRadius theme property.

[4.1.3] #

  • Fixed a bug: Expandables should be unregistered in controller upon their disposal.

[4.1.2] #

  • Optimized comparison operator in ExpandableThemeData.
  • Improved example app.

[4.1.1] #

  • Added iconRotationAngle theme property.

[4.1.0] #

  • Added ability to customize the expand icon in the theme.
  • Converted widget properties tapHeaderToExpand, tapBodyToExpand, hasIcon to theme properties.
  • Added tabBodyToExpand theme property.
  • Added theme property bodyAlignment to specify body alignment.
  • Added a third card to the example app that demonstrates a custom icon.

[4.0.2] #

  • Changed the horizontal alignment of the header and the body of ExpandablePanel to stretch.
  • Fixed an exception when no controller is specified in ExpandableNotifier.

[4.0.1] #

  • Updated pubspec.yaml to require the minimum version of Flutter 1.12.0.

[4.0.0] #

  • Introduced ExpandableTheme and ExpandableThemeData.
  • Deprecated widget-level properties that have been moved to the theme.

[3.0.1] #

  • Added iconColor property to ExpandablePanel

[3.0.0+1] - 06/14/2019 #

  • Updated README and example app.

[3.0.0] - 06/13/2019 #

  • Added ShowOnExpand widget.
  • Moved initialExpanded from ExpandablePanel to ExpandableNotifier.
  • Moved animationDuration from Expandable to ExpandableController.
  • Made ExpandableNotifier to be a stateful widget.
  • Made ExpandablePanel a stateless widget.

[2.2.3] - 06/13/2019 #

  • Added the optional crossFadePoint parameter.

[2.2.2] - 06/09/2019 #

  • Disabled the iconColor parameter until version 1.7.3 is published to stable.

[2.2.1] - 06/09/2019 #

  • Added the optional iconColor parameter to ExpandablePanel.

[2.2.0] - 06/06/2019 #

  • Added the optional headerAlignment parameter to ExpandablePanel.

[2.1.1] - 04/17/2019 #

  • Added the optional key parameter to Expandable and ExpandablePanel.

[2.1.0] - 04/5/2019 #

  • ExpandablePanel does not lose its state if its parent is rebuilt.
  • Example file is moved to example folder for ease of running it.

[2.0.0] - 02/14/2019 #

  • Eliminated the dependency on ScopedModel.
  • Introduced ExpandableNotifier and ExpandableController.
  • This is a breaking change. See README.md for details on migration from 1.x to 2.0.

[1.1.0] - 02/01/2019 #

  • Added ExpandablePanel, a configurable expandable widget with optional header and icon.

[1.0.0] - 01/29/2019 #

  • Initial release.

example/lib/main.dart

import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Expandable Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State createState() {
    return MyHomePageState();
  }
}

class MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Expandable Demo"),
      ),
      body: ExpandableTheme(
        data:
            const ExpandableThemeData(
                iconColor: Colors.blue,
                useInkWell: true,
            ),
        child: ListView(
          physics: const BouncingScrollPhysics(),
          children: <Widget>[
            Card1(),
            Card2(),
            Card3(),
          ],
        ),
      ),
    );
  }
}

const loremIpsum =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

class Card1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ExpandableNotifier(
        child: Padding(
      padding: const EdgeInsets.all(10),
      child: Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          children: <Widget>[
            SizedBox(
              height: 150,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.orange,
                  shape: BoxShape.rectangle,
                ),
              ),
            ),
            ScrollOnExpand(
              scrollOnExpand: true,
              scrollOnCollapse: false,
              child: ExpandablePanel(
                theme: const ExpandableThemeData(
                  headerAlignment: ExpandablePanelHeaderAlignment.center,
                  tapBodyToCollapse: true,
                ),
                header: Padding(
                    padding: EdgeInsets.all(10),
                    child: Text(
                      "ExpandablePanel",
                      style: Theme.of(context).textTheme.body2,
                    )),
                collapsed: Text(
                  loremIpsum,
                  softWrap: true,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                expanded: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    for (var _ in Iterable.generate(5))
                      Padding(
                          padding: EdgeInsets.only(bottom: 10),
                          child: Text(
                            loremIpsum,
                            softWrap: true,
                            overflow: TextOverflow.fade,
                          )),
                  ],
                ),
                builder: (_, collapsed, expanded) {
                  return Padding(
                    padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
                    child: Expandable(
                      collapsed: collapsed,
                      expanded: expanded,
                      theme: const ExpandableThemeData(crossFadePoint: 0),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    ));
  }
}

class Card2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    buildImg(Color color, double height) {
      return SizedBox(
          height: height,
          child: Container(
            decoration: BoxDecoration(
              color: color,
              shape: BoxShape.rectangle,
            ),
          ));
    }

    buildCollapsed1() {
      return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    "Expandable",
                    style: Theme.of(context).textTheme.body1,
                  ),
                ],
              ),
            ),
          ]);
    }

    buildCollapsed2() {
      return buildImg(Colors.lightGreenAccent, 150);
    }

    buildCollapsed3() {
      return Container();
    }

    buildExpanded1() {
      return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    "Expandable",
                    style: Theme.of(context).textTheme.body1,
                  ),
                  Text(
                    "3 Expandable widgets",
                    style: Theme.of(context).textTheme.caption,
                  ),
                ],
              ),
            ),
          ]);
    }

    buildExpanded2() {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            children: <Widget>[
              Expanded(child: buildImg(Colors.lightGreenAccent, 100)),
              Expanded(child: buildImg(Colors.orange, 100)),
            ],
          ),
          Row(
            children: <Widget>[
              Expanded(child: buildImg(Colors.lightBlue, 100)),
              Expanded(child: buildImg(Colors.cyan, 100)),
            ],
          ),
        ],
      );
    }

    buildExpanded3() {
      return Padding(
        padding: EdgeInsets.all(10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              loremIpsum,
              softWrap: true,
            ),
          ],
        ),
      );
    }

    return ExpandableNotifier(
        child: Padding(
      padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
      child: ScrollOnExpand(
        child: Card(
          clipBehavior: Clip.antiAlias,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Expandable(
                collapsed: buildCollapsed1(),
                expanded: buildExpanded1(),
              ),
              Expandable(
                collapsed: buildCollapsed2(),
                expanded: buildExpanded2(),
              ),
              Expandable(
                collapsed: buildCollapsed3(),
                expanded: buildExpanded3(),
              ),
              Divider(
                height: 1,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Builder(
                    builder: (context) {
                      var controller = ExpandableController.of(context);
                      return FlatButton(
                        child: Text(
                          controller.expanded ? "COLLAPSE" : "EXPAND",
                          style: Theme.of(context)
                              .textTheme
                              .button
                              .copyWith(color: Colors.deepPurple),
                        ),
                        onPressed: () {
                          controller.toggle();
                        },
                      );
                    },
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    ));
  }
}

class Card3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    buildItem(String label) {
      return Padding(
        padding: const EdgeInsets.all(10.0),
        child: Text(label),
      );
    }

    buildList() {
      return Column(
        children: <Widget>[
          for (var i in [1, 2, 3, 4]) buildItem("Item ${i}"),
        ],
      );
    }

    return ExpandableNotifier(
        child: Padding(
      padding: const EdgeInsets.all(10),
      child: ScrollOnExpand(
        child: Card(
          clipBehavior: Clip.antiAlias,
          child: Column(
            children: <Widget>[
              ExpandablePanel(
                theme: const ExpandableThemeData(
                  headerAlignment: ExpandablePanelHeaderAlignment.center,
                  tapBodyToExpand: true,
                  tapBodyToCollapse: true,
                  hasIcon: false,
                ),
                header: Container(
                  color: Colors.indigoAccent,
                  child: Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Row(
                      children: [
                        ExpandableIcon(
                          theme: const ExpandableThemeData(
                            expandIcon: Icons.arrow_right,
                            collapseIcon: Icons.arrow_drop_down,
                            iconColor: Colors.white,
                            iconSize: 28.0,
                            iconRotationAngle: math.pi / 2,
                            iconPadding: EdgeInsets.only(right: 5),
                            hasIcon: false,
                          ),
                        ),
                        Expanded(
                          child: Text(
                            "Items",
                            style: Theme.of(context)
                                .textTheme
                                .body2
                                .copyWith(color: Colors.white),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                expanded: buildList(),
              ),
            ],
          ),
        ),
      ),
    ));
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  expandable: ^4.1.4

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:expandable/expandable.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
97
Health:
Code health derived from static analysis. [more]
93
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
96
Learn more about scoring.

We analyzed this package on Jun 3, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.2
  • pana: 0.13.8-dev
  • Flutter: 1.17.1

Health suggestions

Fix lib/expandable.dart. (-6.78 points)

Analysis of lib/expandable.dart reported 14 hints, including:

line 389 col 35: 'animationDuration' is deprecated and shouldn't be used.

line 443 col 37: 'crossFadePoint' is deprecated and shouldn't be used.

line 444 col 32: 'fadeCurve' is deprecated and shouldn't be used.

line 445 col 32: 'sizeCurve' is deprecated and shouldn't be used.

line 446 col 32: 'alignment' is deprecated and shouldn't be used.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 < 3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test