37

I'm trying to get ExpansionTile to collapse after I choose an item, but it does not close the list that was opened.

I tried to use the onExpansionChanged property but I did not succeed

How could you solve this problem?

Insert a gif demonstrating that ExpansionTile does not collapse after choosing an item, and below is also the code used.

enter image description here

import 'package:flutter/material.dart';

void main() {
  runApp(new ExpansionTileSample());
}

class ExpansionTileSample extends StatefulWidget {
  @override
  ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}

class ExpansionTileSampleState extends State<ExpansionTileSample> {
  String foos = 'One';

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('ExpansionTile'),
        ),
        body: new ExpansionTile(
          title: new Text(this.foos),
          backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
          children: <Widget>[
            new ListTile(
              title: const Text('One'),
              onTap: () {
                setState(() {
                  this.foos = 'One';
                });
              },              
            ),
            new ListTile(
              title: const Text('Two'),
              onTap: () {
                setState(() {
                  this.foos = 'Two';
                });
              },              
            ),
            new ListTile(
              title: const Text('Three'),
              onTap: () {
                setState(() {
                  this.foos = 'Three';
                });
              },              
            ),
          ]
        ),
      ),
    );
  }
}
rafaelcb21
  • 12,422
  • 28
  • 62
  • 86

12 Answers12

46

Here is a workaround. Just add a global key (or a value key that changes after selecting an item) and it will force ExpansionTile to rebuild. The downside is losing animation for collapsing.

ExpansionTile(
  key: GlobalKey(),
  title: Text(title),
  children: listTiles,
  ...
)
Edwin Liu
  • 7,413
  • 3
  • 28
  • 27
  • 2
    For more control, the Key can be saved and only reassigned inside a setState when you want to collapse the ExpansionTile. – Attersson Aug 22 '19 at 14:18
  • If the purpose is to rebuild the ExpansionTile every time the title is changed, use a ValueKey with the title value. Is less expansive and it makes more sense. – Juliano May 04 '20 at 21:18
  • 1
    @Juliano, can you provide an example please. I tried your suggestion with following code: `ExpansionTile(title: Container(key: ValueKey(1), child: Row(..)))` but it did not work. This posted answer however does work – Gene Bo Jun 16 '21 at 19:35
  • @GeneBo Try changing the value of the ValueKey when you want to force update the component. – Juliano Oct 20 '21 at 18:37
  • Not working in bottom model sheet – Sneha Mudhigonda Apr 25 '22 at 11:46
  • This is the only solution that worked for me in Bottom Sheet stackoverflow.com/a/65857450/13572333 – Sneha Mudhigonda Apr 25 '22 at 12:30
43

Here is a solution. We just add a expand, collapse and toggle functionality to ExpansionTile.

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';


void main() {
    runApp(new ExpansionTileSample());
}

class ExpansionTileSample extends StatefulWidget {
    @override
    ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}

class ExpansionTileSampleState extends State<ExpansionTileSample> {

    final GlobalKey<AppExpansionTileState> expansionTile = new GlobalKey();
    String foos = 'One';

    @override
    Widget build(BuildContext context) {
        return new MaterialApp(
            home: new Scaffold(
                appBar: new AppBar(
                    title: const Text('ExpansionTile'),
                ),
                body: new AppExpansionTile(
                    key: expansionTile,
                    title: new Text(this.foos),
                    backgroundColor: Theme
                        .of(context)
                        .accentColor
                        .withOpacity(0.025),
                    children: <Widget>[
                        new ListTile(
                            title: const Text('One'),
                            onTap: () {
                                setState(() {
                                    this.foos = 'One';
                                    expansionTile.currentState.collapse();
                                });
                            },
                        ),
                        new ListTile(
                            title: const Text('Two'),
                            onTap: () {
                                setState(() {
                                    this.foos = 'Two';
                                    expansionTile.currentState.collapse();
                                });
                            },
                        ),
                        new ListTile(
                            title: const Text('Three'),
                            onTap: () {
                                setState(() {
                                    this.foos = 'Three';
                                    expansionTile.currentState.collapse();
                                });
                            },
                        ),
                    ]
                ),
            ),
        );
    }
}

// --- Copied and slightly modified version of the ExpansionTile.

const Duration _kExpand = const Duration(milliseconds: 200);

class AppExpansionTile extends StatefulWidget {
    const AppExpansionTile({
        Key key,
        this.leading,
        @required this.title,
        this.backgroundColor,
        this.onExpansionChanged,
        this.children: const <Widget>[],
        this.trailing,
        this.initiallyExpanded: false,
    })
        : assert(initiallyExpanded != null),
            super(key: key);

    final Widget leading;
    final Widget title;
    final ValueChanged<bool> onExpansionChanged;
    final List<Widget> children;
    final Color backgroundColor;
    final Widget trailing;
    final bool initiallyExpanded;

    @override
    AppExpansionTileState createState() => new AppExpansionTileState();
}

class AppExpansionTileState extends State<AppExpansionTile> with SingleTickerProviderStateMixin {
    AnimationController _controller;
    CurvedAnimation _easeOutAnimation;
    CurvedAnimation _easeInAnimation;
    ColorTween _borderColor;
    ColorTween _headerColor;
    ColorTween _iconColor;
    ColorTween _backgroundColor;
    Animation<double> _iconTurns;

    bool _isExpanded = false;

    @override
    void initState() {
        super.initState();
        _controller = new AnimationController(duration: _kExpand, vsync: this);
        _easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
        _easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
        _borderColor = new ColorTween();
        _headerColor = new ColorTween();
        _iconColor = new ColorTween();
        _iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(_easeInAnimation);
        _backgroundColor = new ColorTween();

        _isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
        if (_isExpanded)
            _controller.value = 1.0;
    }

    @override
    void dispose() {
        _controller.dispose();
        super.dispose();
    }

    void expand() {
        _setExpanded(true);
    }

    void collapse() {
        _setExpanded(false);
    }

    void toggle() {
        _setExpanded(!_isExpanded);
    }

    void _setExpanded(bool isExpanded) {
        if (_isExpanded != isExpanded) {
            setState(() {
                _isExpanded = isExpanded;
                if (_isExpanded)
                    _controller.forward();
                else
                    _controller.reverse().then<void>((Null value) {
                        setState(() {
                            // Rebuild without widget.children.
                        });
                    });
                PageStorage.of(context)?.writeState(context, _isExpanded);
            });
            if (widget.onExpansionChanged != null) {
                widget.onExpansionChanged(_isExpanded);
            }
        }
    }

    Widget _buildChildren(BuildContext context, Widget child) {
        final Color borderSideColor = _borderColor.evaluate(_easeOutAnimation) ?? Colors.transparent;
        final Color titleColor = _headerColor.evaluate(_easeInAnimation);

        return new Container(
            decoration: new BoxDecoration(
                color: _backgroundColor.evaluate(_easeOutAnimation) ?? Colors.transparent,
                border: new Border(
                    top: new BorderSide(color: borderSideColor),
                    bottom: new BorderSide(color: borderSideColor),
                )
            ),
            child: new Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                    IconTheme.merge(
                        data: new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
                        child: new ListTile(
                            onTap: toggle,
                            leading: widget.leading,
                            title: new DefaultTextStyle(
                                style: Theme
                                    .of(context)
                                    .textTheme
                                    .subhead
                                    .copyWith(color: titleColor),
                                child: widget.title,
                            ),
                            trailing: widget.trailing ?? new RotationTransition(
                                turns: _iconTurns,
                                child: const Icon(Icons.expand_more),
                            ),
                        ),
                    ),
                    new ClipRect(
                        child: new Align(
                            heightFactor: _easeInAnimation.value,
                            child: child,
                        ),
                    ),
                ],
            ),
        );
    }

    @override
    Widget build(BuildContext context) {
        final ThemeData theme = Theme.of(context);
        _borderColor.end = theme.dividerColor;
        _headerColor
            ..begin = theme.textTheme.subhead.color
            ..end = theme.accentColor;
        _iconColor
            ..begin = theme.unselectedWidgetColor
            ..end = theme.accentColor;
        _backgroundColor.end = widget.backgroundColor;

        final bool closed = !_isExpanded && _controller.isDismissed;
        return new AnimatedBuilder(
            animation: _controller.view,
            builder: _buildChildren,
            child: closed ? null : new Column(children: widget.children),
        );
    }
}
Simon
  • 10,932
  • 50
  • 49
  • 2
    Thanks, it works perfectly, even if I have two `ExpansionTile` open, it will only collapse the one that was chosen the item. But for this each `ExpansionTile` will have to have its own `GlobalKey expansionTile` such as `GlobalKey expansionTile1` – rafaelcb21 Feb 23 '18 at 00:41
  • 20
    I don't understand why they don't have this in the build in widgets – Burhanuddin Rashid May 11 '18 at 09:44
  • I'm having this problem with AppExpansionTile: I/flutter (14705):EXCEPTION CAUGHT BY WIDGETS LIBRARY I/flutter (14705): The following assertion was thrown building NotificationListener: I/flutter (14705): Multiple widgets used the same GlobalKey. I/flutter (14705): The key A GlobalKey can only be specified on one widget at a time in the widget tree.... Would you help me? – Oséias Ribeiro Apr 06 '19 at 20:49
  • Another variant (to avoid using GlobalKey) is to add `static AppExpansionTileState of(BuildContext context) { return context.ancestorStateOfType(const TypeMatcher()); }` to AppExpansionTile class and the simply use `AppExpansionTile.of(context).collapse();` – Pavel Jul 26 '19 at 12:31
  • Hi, How to make it automatically close if there are multiple tiles on opening one tile other one should get closed – praveen Dp Sep 09 '19 at 07:27
  • @Pavel TypeMatcher and ancestorStateOfType deprecate. – BIS Tech Jul 05 '20 at 05:28
  • @BloodLoss yes, now `context.ancestorStateOfType(const TypeMatcher())` should be replaced with `context.findAncestorStateOfType();` – Pavel Jul 05 '20 at 23:05
  • Maybe an easier solution would be to make your class statefull and to update a dummy state. Then in the onTap to update that dummy state. Then there is no need to make your own widget. The setState would trigger the same thing this answer does (reload the page and closing the ExpensionTile) – SilkeNL Mar 22 '21 at 12:50
10

solution below would work, but it is quite hacky and might not be the best one:



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

    void main() {
      runApp(new ExpansionTileSample());
    }

    class ExpansionTileSample extends StatefulWidget {
      @override
      ExpansionTileSampleState createState() => new ExpansionTileSampleState();
    }

    class ExpansionTileSampleState extends State {
      String foos = 'One';
      int _key;

      _collapse() {
        int newKey;
        do {
          _key = new Random().nextInt(10000);
        } while(newKey == _key);
      }

      @override
      void initState() {
        super.initState();
        _collapse();
      }

      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          home: new Scaffold(
            appBar: new AppBar(
              title: const Text('ExpansionTile'),
            ),
            body: new ExpansionTile(
                key: new Key(_key.toString()),
                initiallyExpanded: false,
                title: new Text(this.foos),
                backgroundColor: Theme
                    .of(context)
                    .accentColor
                    .withOpacity(0.025),
                children: [
                  new ListTile(
                    title: const Text('One'),
                    onTap: () {
                      setState(() {
                        this.foos = 'One';
                        _collapse();
                      });
                    },
                  ),
                  new ListTile(
                    title: const Text('Two'),
                    onTap: () {
                      setState(() {
                        this.foos = 'Two';
                        _collapse();
                      });
                    },
                  ),
                  new ListTile(
                    title: const Text('Three'),
                    onTap: () {
                      setState(() {
                        this.foos = 'Three';
                        _collapse();
                      });
                    },
                  ),
                ]
            ),
          ),
        );
      }
    }

I found that ExpansionTile has initiallyExpanded property, which is the only way to make it collapsed. As property works only initially you want to make ExpansionTile to be recreated everytime build is called. To force it you just assign different key everytime you build it. This might not be best solution performance wise, but ExpansionTile is quite simple, so this should not be a problem.

Alex Radzishevsky
  • 3,416
  • 2
  • 14
  • 25
  • Thanks, it works if you only have one `ExpansionTile`, if I inserted two `ExpansionTile` and open both. When choosing the item from one of them, the two collapse and not just the one that was chosen the item. – rafaelcb21 Feb 23 '18 at 00:37
  • I wrote an article on a LanguageSelector Widget that implements your solution. Here is the link to the article: [link](https://www.didierboelens.com/2018/04/internationalization---language-selector-widget-with-auto-collapse/) – boeledi Apr 21 '18 at 16:16
  • your example working great when your expansion tiles have dynamic items and you need to create the dynamic global key. above exapmle, I tried but giving the error of `same global key`. I get rid of this situation by applying your code. – Farhana Naaz Ansari Mar 13 '19 at 07:27
  • how can i handle collapse all the other opened tiles but the one clicked by the user? – Zubair Rehman Aug 22 '19 at 07:01
  • @praveenDp not yet, i deferred it for time being but will come up with a solution really soon – Zubair Rehman Sep 10 '19 at 03:38
8

None of the provided solutions pleased me.

I ended up creating a custom ExpandableListTile. As you can see below, its code is very brief and easy to customize.

I also had to create two supporting classes (that only handle the required animations) to build my widget:

  • ExpandableSection: a widget that can be easily controlled by one parameter "expanded".
  • RotatableSection: a widget to rotate the "Expand More" icon based on one parameter.

The main class:

class ExpandableListTile extends StatelessWidget {
  const ExpandableListTile({Key key, this.title, this.expanded, this.onExpandPressed, this.child}) : super(key: key);

  final Widget title;
  final bool expanded;
  final Widget child;
  final Function onExpandPressed;

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      ListTile(
        title: title,
        onTap: onExpandPressed,
        trailing: IconButton(
          onPressed: onExpandPressed,
          // icon: Icon(Icons.expand_more),
          icon: RotatableSection(
             rotated: expanded,
             child: SizedBox(height: 30, width: 30, child: Icon(Icons.expand_more),) 
          ),
        ),
      ),
      ExpandableSection(child: child, expand: expanded,)
    ]);
  }
}

Usage (simplified):

//...
return ExpandableListTile(
  onExpandPressed: (){ setState((){ _expandedItem = 0;}) },
  title: Text('Item'),
  expanded: _expandedItem==0,
  child: Padding(
    padding: const EdgeInsets.fromLTRB(8,0,0,0),
    child: Container(
      color: Color.fromRGBO(0, 0, 0, .2),
      child: Column(children: <Widget>[
        ListTile(title: Text('Item 1')),
        ListTile(title: Text('Item 2')),
        ListTile(title: Text('Item 3')),
        ListTile(title: Text('Item 4'))
      ],),
    ),
  ),
),
//...

The ExpandableSection class:

class ExpandableSection extends StatefulWidget {

  final Widget child;
  final bool expand;
  ExpandableSection({this.expand = false, this.child});

  @override
  _ExpandableSectionState createState() => _ExpandableSectionState();
}

class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
  AnimationController animationController;
  Animation<double> sizeAnimation; 
  Animation<double> opacityAnimation; 

  @override
  void initState() {
    super.initState();
    prepareAnimations();
    _runExpandCheck();
  }

  ///Setting up the animation
  void prepareAnimations() {
    animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),);
    sizeAnimation = CurvedAnimation(parent: animationController, curve: Curves.fastOutSlowIn,);
    opacityAnimation = CurvedAnimation(parent: animationController, curve: Curves.slowMiddle,);
  }

  void _runExpandCheck() {
    if(widget.expand) { animationController.forward(); }
    else { animationController.reverse(); }
  }

  @override
  void didUpdateWidget(ExpandableSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    _runExpandCheck();
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: opacityAnimation,
      child: SizeTransition(
        axisAlignment: 1.0,
        sizeFactor: sizeAnimation,
        child: widget.child
      )
    );
  }
}

The RotatableSection class:

class RotatableSection extends StatefulWidget {

  final Widget child;
  final bool rotated;
  final double initialSpin;
  final double endingSpin;
  RotatableSection({this.rotated = false, this.child, this.initialSpin=0, this.endingSpin=0.5});

  @override
  _RotatableSectionState createState() => _RotatableSectionState();
}

class _RotatableSectionState extends State<RotatableSection> with SingleTickerProviderStateMixin {
  AnimationController animationController;
  Animation<double> animation; 

  @override
  void initState() {
    super.initState();
    prepareAnimations();
    _runCheck();
  }

  final double _oneSpin = 6.283184;

  ///Setting up the animation
  void prepareAnimations() {
    animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300), 
      lowerBound: _oneSpin * widget.initialSpin, upperBound: _oneSpin * widget.endingSpin, );
    animation = CurvedAnimation( parent: animationController, curve: Curves.linear, );
  }

  void _runCheck() {
    if(widget.rotated) { animationController.forward(); }
    else { animationController.reverse(); }
  }

  @override
  void didUpdateWidget(RotatableSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    _runCheck();
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: animationController,
        child: widget.child,
        builder: (BuildContext context, Widget _widget) {
          return new Transform.rotate(
            angle: animationController.value,
            child: _widget,
          );
      },
    );
  }
}
Juliano
  • 2,422
  • 21
  • 23
  • I was about to do the same, you just saved my day :) Thanks for sharing! – A.Rhman Sayes Feb 26 '20 at 11:52
  • I'm glad it helped you. You could return the favor by just upvoting my answer. :) – Juliano Feb 27 '20 at 00:51
  • one it's expanded, after doesn't collapse. – Sumit Pansuriya Apr 30 '20 at 05:33
  • @SumitPansuriya, you just need to change the logic in onExpandPressed to make the index assume a negative value (-1) when it already is equal to 0. The usage sample is very simple, just to show how you can control the ExpandableListTile by its parent. All you have to do is to change the expanded boolean property. – Juliano Apr 30 '20 at 21:13
  • I use InkWell on the ExpandableListTile main Colums to toggle when title is clicked – Elia Weiss Sep 24 '20 at 09:53
3

Use this package and follow my code. Hope this will help you :). Easy to use. https://pub.dev/packages/expansion_tile_card/example

final List<GlobalKey<ExpansionTileCardState>> cardKeyList = [];

     ...    ListView.builder(
              itemCount: 10,
              itemBuilder: (BuildContext context, int index) {
                cardKeyList.add(GlobalKey(debugLabel: "index :$index"));
                return ExpansionTileCard(
                  title: Text('title'),
                  key: cardKeyList[index],
                  onExpansionChanged: (value) {
                    if (value) {
                      Future.delayed(const Duration(milliseconds: 500), () {
                        for (var i = 0; i < cardKeyList.length; i++) {
                          if (index != i) {
                            cardKeyList[i].currentState?.collapse();
                          }
                        }
                      });
                    }
                  },
                );
              }),
3

Use UniqueKey:

ExpansionTile(
  key: UniqueKey(),
  // Other properties
)
iDecode
  • 22,623
  • 19
  • 99
  • 186
1

I've made a TreeView widget. It uses ExpansionTile to simulate the hierarchy. Each ExpansionTile could host a collection of ExpansionTile which can host ...etc.

Everything worked fine until I wanted to add 2 features : expand all / collapse all. What helped me to overcame this problem is the GlobalKey.

My TreeView widget, is hosted in a page and is used with a global key. I expose a VoidCallback. The implementation sets a new key in the setState method.

// TreeView host page
GlobalKey<TreeViewState> _key = GlobalKey();
void redrawWidgetCallback() {
    setState(() {
      // Triggers a rebuild of the whole TreeView.
      _key = GlobalKey();
    });
}
[...]
// In the Scaffold body :
TreeView(
    key: _key,
    treeViewItems: widget.treeViewItems,
    redrawWidgetCallback: redrawWidgetCallback,
  )

Then in my collapse/expand method in the widget, at the end, I call widget.redrawWidgetCallback. No need to deal with a key for each level of the treeView : the root element widget is enough.

It may have perf issues / not the right way to go. But since my TreeView won't be used with more than 50 nodes, it's ok for me until I found a better solution which doesn't involve to create an ExpandableTile because I believe this behavior will be available oneday on the ExpansionTile itself.

PS : notice that this workaround doesn't run the expand animation.

Nk54
  • 751
  • 7
  • 15
1

Create a clone from ExpansionTile class and replace build method code by the following:

@override
Widget build(BuildContext context) {
  final bool closed = !_isExpanded && _controller.isDismissed;
  return AnimatedBuilder(
    animation: _controller.view,
    builder: _buildChildren,
    child: closed ? null : GestureDetector(
      child: Column(children: widget.children),
      onTap: _handleTap,
    ),
  );
}

and then ExpansionTile will collapse after click on each item.

Note: if one of children has onTap call back, this solution doesn't work. in this case you must provide onChildTap handler to pass index of tapped child in use case.(contact me for complete code)

Hossein
  • 797
  • 1
  • 8
  • 24
1

I have modified the custom code, And its works fine for me.

Here is the solution.

 // Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

const Duration _kExpand = Duration(milliseconds: 200);

/// A single-line [ListTile] with a trailing button that expands or collapses
/// the tile to reveal or hide the [children].
///
/// This widget is typically used with [ListView] to create an
/// "expand / collapse" list entry. When used with scrolling widgets like
/// [ListView], a unique [PageStorageKey] must be specified to enable the
/// [AppExpansionTile] to save and restore its expanded state when it is scrolled
/// in and out of view.
///
/// This class overrides the [ListTileTheme.iconColor] and [ListTileTheme.textColor]
/// theme properties for its [ListTile]. These colors animate between values when
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
/// between [textColor] and [collapsedTextColor].
///
/// See also:
///
///  * [ListTile], useful for creating expansion tile [children] when the
///    expansion tile represents a sublist.
///  * The "Expand and collapse" section of
///    <https://material.io/components/lists#types>
class AppExpansionTile extends StatefulWidget {
  /// Creates a single-line [ListTile] with a trailing button that expands or collapses
  /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
  /// be non-null.
  const AppExpansionTile({
    GlobalKey<AppExpansionTileState>? key,
    this.leading,
    required this.title,
    this.subtitle,
    this.onExpansionChanged,
    this.children = const <Widget>[],
    this.trailing,
    this.initiallyExpanded = false,
    this.maintainState = false,
    this.tilePadding,
    this.expandedCrossAxisAlignment,
    this.expandedAlignment,
    this.childrenPadding,
    this.backgroundColor,
    this.collapsedBackgroundColor,
    this.textColor,
    this.collapsedTextColor,
    this.iconColor,
    this.collapsedIconColor,
  })  : assert(initiallyExpanded != null),
        assert(maintainState != null),
        assert(
          expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
          'CrossAxisAlignment.baseline is not supported since the expanded children '
          'are aligned in a column, not a row. Try to use another constant.',
        ),
        super(key: key);

  /// A widget to display before the title.
  ///
  /// Typically a [CircleAvatar] widget.
  final Widget? leading;

  /// The primary content of the list item.
  ///
  /// Typically a [Text] widget.
  final Widget title;

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
  final Widget? subtitle;

  /// Called when the tile expands or collapses.
  ///
  /// When the tile starts expanding, this function is called with the value
  /// true. When the tile starts collapsing, this function is called with
  /// the value false.
  final ValueChanged<bool>? onExpansionChanged;

  /// The widgets that are displayed when the tile expands.
  ///
  /// Typically [ListTile] widgets.
  final List<Widget> children;

  /// The color to display behind the sublist when expanded.
  final Color? backgroundColor;

  /// When not null, defines the background color of tile when the sublist is collapsed.
  final Color? collapsedBackgroundColor;

  /// A widget to display instead of a rotating arrow icon.
  final Widget? trailing;

  /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
  final bool initiallyExpanded;

  /// Specifies whether the state of the children is maintained when the tile expands and collapses.
  ///
  /// When true, the children are kept in the tree while the tile is collapsed.
  /// When false (default), the children are removed from the tree when the tile is
  /// collapsed and recreated upon expansion.
  final bool maintainState;

  /// Specifies padding for the [ListTile].
  ///
  /// Analogous to [ListTile.contentPadding], this property defines the insets for
  /// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
  /// the expanded [children] widgets.
  ///
  /// When the value is null, the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
  final EdgeInsetsGeometry? tilePadding;

  /// Specifies the alignment of [children], which are arranged in a column when
  /// the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and [Align] widget to align the column. The `expandedAlignment`
  /// parameter is passed directly into the [Align].
  ///
  /// Modifying this property controls the alignment of the column within the
  /// expanded tile, not the alignment of [children] widgets within the column.
  /// To align each child within [children], see [expandedCrossAxisAlignment].
  ///
  /// The width of the column is the width of the widest child widget in [children].
  ///
  /// When the value is null, the value of `expandedAlignment` is [Alignment.center].
  final Alignment? expandedAlignment;

  /// Specifies the alignment of each child within [children] when the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
  ///
  /// Modifying this property controls the cross axis alignment of each child
  /// within its [Column]. Note that the width of the [Column] that houses
  /// [children] will be the same as the widest child widget in [children]. It is
  /// not necessarily the width of [Column] is equal to the width of expanded tile.
  ///
  /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
  /// instead.
  ///
  /// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
  final CrossAxisAlignment? expandedCrossAxisAlignment;

  /// Specifies padding for [children].
  ///
  /// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
  final EdgeInsetsGeometry? childrenPadding;

  /// The icon color of tile's [trailing] expansion icon when the
  /// sublist is expanded.
  ///
  /// Used to override to the [ListTileTheme.iconColor].
  final Color? iconColor;

  /// The icon color of tile's [trailing] expansion icon when the
  /// sublist is collapsed.
  ///
  /// Used to override to the [ListTileTheme.iconColor].
  final Color? collapsedIconColor;

  /// The color of the tile's titles when the sublist is expanded.
  ///
  /// Used to override to the [ListTileTheme.textColor].
  final Color? textColor;

  /// The color of the tile's titles when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileTheme.textColor].
  final Color? collapsedTextColor;

  @override
  AppExpansionTileState createState() => AppExpansionTileState();
}

class AppExpansionTileState extends State<AppExpansionTile>
    with SingleTickerProviderStateMixin {
  static final Animatable<double> _easeOutTween =
      CurveTween(curve: Curves.easeOut);
  static final Animatable<double> _easeInTween =
      CurveTween(curve: Curves.easeIn);
  static final Animatable<double> _halfTween =
      Tween<double>(begin: 0.0, end: 0.5);

  final ColorTween _borderColorTween = ColorTween();
  final ColorTween _headerColorTween = ColorTween();
  final ColorTween _iconColorTween = ColorTween();
  final ColorTween _backgroundColorTween = ColorTween();

  late AnimationController _controller;
  late Animation<double> _iconTurns;
  late Animation<double> _heightFactor;
  late Animation<Color?> _borderColor;
  late Animation<Color?> _headerColor;
  late Animation<Color?> _iconColor;
  late Animation<Color?> _backgroundColor;

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: _kExpand, vsync: this);
    _heightFactor = _controller.drive(_easeInTween);
    _iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
    _borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
    _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
    _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
    _backgroundColor =
        _controller.drive(_backgroundColorTween.chain(_easeOutTween));

    _isExpanded = PageStorage.of(context)?.readState(context) as bool? ??
        widget.initiallyExpanded;
    if (_isExpanded) _controller.value = 1.0;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void expand() {
    _isExpanded = true;
    handleTap();
  }

  void collapse() {
    _isExpanded = false;
    handleTap();
  }

  @override
  void didUpdateWidget(covariant AppExpansionTile oldWidget) {
    if (widget.initiallyExpanded) {
      expand();
    } else {
      collapse();
    }
    super.didUpdateWidget(oldWidget);
  }

  void handleTap() {
    setState(() {
      if (_isExpanded) {
        _controller.forward();
      } else {
        _controller.reverse().then<void>((void value) {
          if (!mounted) return;
          setState(() {
            // Rebuild without widget.children.
          });
        });
      }
      PageStorage.of(context)?.writeState(context, _isExpanded);
    });
    // if (widget.onExpansionChanged != null)
    //   widget.onExpansionChanged!(_isExpanded);
  }

  Widget _buildChildren(BuildContext context, Widget? child) {
    final Color borderSideColor = _borderColor.value ?? Colors.transparent;

    return Container(
      decoration: BoxDecoration(
        color: _backgroundColor.value ?? Colors.transparent,
        border: Border(
          top: BorderSide(color: borderSideColor),
          bottom: BorderSide(color: borderSideColor),
        ),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTileTheme.merge(
            iconColor: _iconColor.value,
            textColor: _headerColor.value,
            child: ListTile(
              onTap: () {
                if (widget.onExpansionChanged != null) {
                  widget.onExpansionChanged!(_isExpanded);
                }
              },
              contentPadding: widget.tilePadding,
              leading: widget.leading,
              title: widget.title,
              subtitle: widget.subtitle,
              trailing: widget.trailing ??
                  RotationTransition(
                    turns: _iconTurns,
                    child: const Icon(Icons.expand_more),
                  ),
            ),
          ),
          ClipRect(
            child: Align(
              alignment: widget.expandedAlignment ?? Alignment.center,
              heightFactor: _heightFactor.value,
              child: child,
            ),
          ),
        ],
      ),
    );
  }

  @override
  void didChangeDependencies() {
    final ThemeData theme = Theme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    _borderColorTween.end = theme.dividerColor;
    _headerColorTween
      ..begin = widget.collapsedTextColor ?? theme.textTheme.subtitle1!.color
      ..end = widget.textColor ?? colorScheme.secondary;
    _iconColorTween
      ..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor
      ..end = widget.iconColor ?? colorScheme.secondary;
    _backgroundColorTween
      ..begin = widget.collapsedBackgroundColor
      ..end = widget.backgroundColor;
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    final bool closed = !_isExpanded && _controller.isDismissed;
    final bool shouldRemoveChildren = closed && !widget.maintainState;

    final Widget result = Offstage(
      child: TickerMode(
        child: Padding(
          padding: widget.childrenPadding ?? EdgeInsets.zero,
          child: Column(
            crossAxisAlignment:
                widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
            children: widget.children,
          ),
        ),
        enabled: !closed,
      ),
      offstage: closed,
    );

    return AnimatedBuilder(
      animation: _controller.view,
      builder: _buildChildren,
      child: shouldRemoveChildren ? null : result,
    );
  }
}

Usage

late int _tileIndex=-1;
    return AppExpansionTile(
          title: Text(
           'Tile $index',
          tilePadding: const EdgeInsets.symmetric(horizontal: 24),
          initiallyExpanded: _tileIndex == index,
          onExpansionChanged: (s) {
            if (_tileIndex == index) {
              _tileIndex = -1;
              setState(() {});
            } else {
              setState(() {
                _tileIndex = index!;
              });
            }
          },
    );
satish
  • 1,304
  • 3
  • 13
  • 33
1

I think it is impossible with expansion tile but, there's a package named accordion and has much more comfortabilities. Link:https://pub.dev/packages/accordion

Wodota ML
  • 76
  • 5
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31283491) – Ethan Mar 18 '22 at 16:33
1

After spending more time than i wish on this, i finally found a native solution and since it has been 4 years i'll show my own example.

class ExpansionTileEvent extends StatefulWidget {
  const ExpansionTileEvent({super.key});

  @override
  State<ExpansionTileEvent> createState() => _ExpansionTileEventState();
}

class _ExpansionTileEventState extends State<ExpansionTileEvent> {
  List<String> list = <String>[
    "Item 1",
    "Item 2.",
    "Item 3",
  ];

  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    return ExpansionTile(
      title: const Text("Select an item"),
      subtitle: Text(appState.selectedEvent),
      children: list.map((String value) {
        return Builder(builder: (context) {
          return ListTile(
            title: Text(value),
            onTap: () {
              // This is called when the user selects an item.
              setState(() {
                appState.selectedEvent = value;
                ExpansionTileController.of(context).collapse();
              });
            },
          );
        });
      }).toList(),
    );
  }
}`

Wrap the ListTiles with a builder and the ExpansionTileController sets the .collapse from context.

Talis
  • 11
  • 1
0

For List of items using @simon solution

List<GlobalKey<AppExpansionTileState> > expansionTile;

instantiate your expansionTile

expansionTile=List<GlobalKey<AppExpansionTileState>>.generate(listItems.length, (index) => GlobalKey());

and use like so inside a ListView.builder()

                               key: expansionTile[index],
                                onExpansionChanged: (value) {
                                  if (value) {
                                    for (var tileKey in expansionTile) {
                                      if (tileKey.currentState !=
                                          expansionTile[index]
                                              .currentState) {
                                        tileKey.currentState.collapse();
                                      } else {
                                        tileKey.currentState.expand();
                                      }
                                    }
                                  }
                                },
Gstuntz
  • 434
  • 4
  • 10