top of page
  • Writer's pictureAlex Fourman

Flutter How To - Expandable List Tile

TL;DR here is the GitHub project with the implementation and a demo.


Showing information in a list is a very common pattern and often enough the list item will only show the most prominent details to save screen real-estate. Most apps will then show any extra details in a new screen. This decision has mostly been influenced by the difficulty to implement another approach, which is to show extra details still in context with the list. Flutter enables these animations in a way that is far easier to implement.


This is what it will look like


Well first we need to break it all down to the main components. The way I see it we have our ExpandListTile widget which will need to receive the widget the is shown when it's collapsed and the widget that's shown when it's expanded. So we will be switching between those two on a tap on ExpandListTile. Now to add the animation we have the size animation where the container grows from collapsed to expanded and shrinks when reversed. The last animation to take care of would be cross-fading between the collapsed and the expanded widgets.

So to sum it up:

  1. switch between collapsed and expanded on tap

  2. animate the size between collapsed and expanded

  3. animate the cross-fade between collapsed and expanded

While there are different way to implement this effect, I'll demonstrate the simpler way using implicit animations, using AnimatedContainer and AnimatedSwitcher.

switch between collapsed and expanded on tap

Making our ExpandListTile statefull where each tap sets a new state, expanded or collapsed.

animate the size between collapsed and expanded

AnimatedContainer's height parameter which is also tied to our state will trigger the container's animation

animate the cross-fade between collapsed and expanded

AnimatedSwitcher will animate new children


import 'package:flutter/material.dart';

class ExpandListTile extends StatefulWidget {
  const ExpandListTile(
      {Key key,
      this.expandedHeight = 300.0,
      this.collapsedHeight = 70.0,
      this.duration = const Duration(milliseconds: 200),
      this.curve = Curves.ease})
      : super(key: key);

  final Widget expandedChild;
  final Widget collapsedChild;
  final double expandedHeight;
  final double collapsedHeight;
  final Duration duration;
  final Curve curve;

  _ExpandListTileState createState() => _ExpandListTileState();

class _ExpandListTileState extends State<ExpandListTile> {
  bool _isExpanded = false;

  Widget build(BuildContext context) {
    final child = _isExpanded ? 
                  widget.expandedChild : 
    return GestureDetector(
      //IMPORTANT: allows "empty" spaces to respond to events
      behavior: HitTestBehavior.translucent, 
      onTap: (() => setState(() => _isExpanded = !_isExpanded)),
      child: AnimatedContainer(
        curve: widget.curve,
        height: _isExpanded ? 
                widget.expandedHeight : 
        duration: widget.duration,
        child: AnimatedSwitcher(
          duration: widget.duration,
          child: OverflowBox(
            key: ValueKey(_isExpanded),
            alignment: Alignment.topLeft,
                _isExpanded ? 
                widget.expandedHeight : 
            child: child,


Why not just use ExpansionPanelList

While it is definitely one way to go about it, I don't like the fact that the title is always visible and that there is a dropdown icon.

Why GestureDetector has behavior: HitTestBehavior.translucent?

This ensures that when tapping on an empty space will still be intercepted.

Why is there an OverflowBox?

while changing the size of the container we want the height of the expanded and collapsed children to be their final size, to avoid widgets changing layout. I find this have a more intuitive look.

If you have a different way or any thoughts on this implementation, leave me a comment, I'd like to hear what you think.

680 views0 comments

Recent Posts

See All
bottom of page