How to create Expandable ListView in Flutter
Asked Answered
S

3

44

How to make an Expandable ListView using Flutter like the screenshot below?

I want to make a scrollable list view of ExpansionTileswhich when expanded shows a non-scrollable list view.

I tried to implement list view of ExpansionTiles inside which I nested another list view using listView.builder(...). But when I expanded the ExpansionTile the list view didn't show up...

Expandable ListView

(The screenshot is for illustrative purpose)

Is there a way to get similar output in Flutter?

EDIT: My Source Code:

import 'package:flutter/material.dart';

void main() => runApp(
  new MaterialApp(
    home: new MyApp(),
  )
);

var data = {
  "01/01/2018": [
    ["CocaCola", "\$ 5"],
    ["Dominos Pizza", "\$ 50"],
  ],

  "04/01/2018": [
    ["Appy Fizz", "\$ 10"],
    ["Galaxy S9+", "\$ 700"],
    ["Apple iPhone X", "\$ 999"],
  ],
};

List<String> dataKeys = data.keys.toList();

String getFullDate(String date) {
  List<String> dateSplit = date.split('/');
  List<String> months = ["Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sep", "Oct", "Nov", "Dec"];

  return "${dateSplit[0]} ${months[int.parse(dateSplit[1]) - 1]} ${dateSplit[2]}";
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<Widget> _buildList(int keyIndex) {
    List<Widget> list = [];

    for (int i = 0; i < data[dataKeys[keyIndex]].length; i++) {
      list.add(
        new Row(
          children: <Widget>[
            new CircleAvatar(
              child: new Icon(Icons.verified_user),
              radius: 20.0,
            ),
            new Text(data[dataKeys[keyIndex]][i][0])
          ],
        )
      );
    }

    return list;
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Expense Monitor"),
      ),
      body: new Container (
        child: new ListView.builder(
          itemCount: dataKeys.length,
          itemBuilder: (BuildContext context, int keyIndex) {
            return new Card(
              child: new ExpansionTile(
                title: new Text(getFullDate(dataKeys[keyIndex])),
                children: <Widget>[
                  new Column(
                    children: _buildList(keyIndex)
                  )
                ]
              ),
            );
          }
        )
      )
    );
  }
}

Error as shown in Console:

I/flutter (12945): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
I/flutter (12945): The following assertion was thrown during performResize():
I/flutter (12945): Vertical viewport was given unbounded height.
I/flutter (12945): Viewports expand in the scrolling direction to fill their container.In this case, a vertical
I/flutter (12945): viewport was given an unlimited amount of vertical space in which to expand. This situation
I/flutter (12945): typically happens when a scrollable widget is nested inside another scrollable widget.
I/flutter (12945): If this widget is always nested in a scrollable widget there is no need to use a viewport because
I/flutter (12945): there will always be enough vertical space for the children. In this case, consider using a Column
I/flutter (12945): instead. Otherwise, consider using the "shrinkWrap" property (or a ShrinkWrappingViewport) to size
I/flutter (12945): the height of the viewport to the sum of the heights of its children.
I/flutter (12945): When the exception was thrown, this was the stack:
I/flutter (12945): #0      RenderViewport.performResize.<anonymous closure> (package:flutter/src/rendering/viewport.dart:944:15)
I/flutter (12945): #1      RenderViewport.performResize (package:flutter/src/rendering/viewport.dart:997:6)
I/flutter (12945): #2      RenderObject.layout (package:flutter/src/rendering/object.dart:1555:9)
I/flutter (12945): #3      _RenderProxyBox&RenderBox&RenderObjectWithChildMixin&RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:109:13)
......
I/flutter (12945): ════════════════════════════════════════════════════════════════════════════════════════════════════
I/flutter (12945): Another exception was thrown: RenderBox was not laid out: RenderViewport#df29c NEEDS-LAYOUT NEEDS-PAINT
Switzerland answered 25/5, 2018 at 13:17 Comment(4)
Please add your code. Did you get errors in the console?Dishonorable
@GünterZöchbauer I have included the code and stack trace.Switzerland
I think it was a bug in Flutter. The problem was fixed after I deleted the build folder and rebuilt the project.Switzerland
Ir's a known issue since a while that some changes don't take effect without flutter clean which deletes the build folder but that should be fixed in master at least.Dishonorable
A
60

Try this:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: MyApp(), debugShowCheckedModeBanner: false,),);

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: vehicles.length,
        itemBuilder: (context, i) {
          return ExpansionTile(
            title: Text(vehicles[i].title, style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),),
            children: <Widget>[
              Column(
                children: _buildExpandableContent(vehicles[i]),
              ),
            ],
          );
        },
      ),
    );
  }

  _buildExpandableContent(Vehicle vehicle) {
    List<Widget> columnContent = [];

    for (String content in vehicle.contents)
      columnContent.add(
        ListTile(
          title: Text(content, style: TextStyle(fontSize: 18.0),),
          leading: Icon(vehicle.icon),
        ),
      );

    return columnContent;
  }
}

class Vehicle {
  final String title;
  List<String> contents = [];
  final IconData icon;

  Vehicle(this.title, this.contents, this.icon);
}

List<Vehicle> vehicles = [
  Vehicle(
    'Bike',
    ['Vehicle no. 1', 'Vehicle no. 2', 'Vehicle no. 7', 'Vehicle no. 10'],
    Icons.motorcycle,
  ),
  Vehicle(
    'Cars',
    ['Vehicle no. 3', 'Vehicle no. 4', 'Vehicle no. 6'],
    Icons.directions_car,
  ),
];
Aspasia answered 26/5, 2018 at 6:56 Comment(4)
I accepted this answer since my approach was close enough and this was easy to implement.Switzerland
If you want the arrow in the left side simply open ExpansionTile class and copy everything inside it and rename the class and state to something else. Then replace trailing property with leading. Remove all imports and press Alt + Space for auto suggestion and import the suggested imports. Finally use this CustomExpansionTile instead of original one to get arrow on left.Aspasia
how to make this only one list expandable at a time?Fracture
Thank you @VinothKumar . Your code helped me a lotMaecenas
N
29

Try this:

First Make an ExpandableContainer using AnimatedContainer.

Then Make an ExpandableListView which will create a Column . The first child of Column will be a button to expand and Second will be ExpandableContainer . ExpandableContainer will have a ListView as its child.

The last step will be to make a ListView of ExpandableListView.

The Result :

ExpandableLIstView

The Code :

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

void main() {
  runApp(new MaterialApp(home: new Home()));
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: Colors.grey,
      appBar: new AppBar(
        title: new Text("Expandable List"),
        backgroundColor: Colors.redAccent,
      ),
      body: new ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return new ExpandableListView(title: "Title $index");
        },
        itemCount: 5,
      ),
    );
  }
}

class ExpandableListView extends StatefulWidget {
  final String title;

  const ExpandableListView({Key key, this.title}) : super(key: key);

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

class _ExpandableListViewState extends State<ExpandableListView> {
  bool expandFlag = false;

  @override
  Widget build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 1.0),
      child: new Column(
        children: <Widget>[
          new Container(
            color: Colors.blue,
            padding: new EdgeInsets.symmetric(horizontal: 5.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                new IconButton(
                    icon: new Container(
                      height: 50.0,
                      width: 50.0,
                      decoration: new BoxDecoration(
                        color: Colors.orange,
                        shape: BoxShape.circle,
                      ),
                      child: new Center(
                        child: new Icon(
                          expandFlag ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
                          color: Colors.white,
                          size: 30.0,
                        ),
                      ),
                    ),
                    onPressed: () {
                      setState(() {
                        expandFlag = !expandFlag;
                      });
                    }),
                new Text(
                  widget.title,
                  style: new TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
                )
              ],
            ),
          ),
          new ExpandableContainer(
              expanded: expandFlag,
              child: new ListView.builder(
                itemBuilder: (BuildContext context, int index) {
                  return new Container(
                    decoration:
                        new BoxDecoration(border: new Border.all(width: 1.0, color: Colors.grey), color: Colors.black),
                    child: new ListTile(
                      title: new Text(
                        "Cool $index",
                        style: new TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
                      ),
                      leading: new Icon(
                        Icons.local_pizza,
                        color: Colors.white,
                      ),
                    ),
                  );
                },
                itemCount: 15,
              ))
        ],
      ),
    );
  }
}

class ExpandableContainer extends StatelessWidget {
  final bool expanded;
  final double collapsedHeight;
  final double expandedHeight;
  final Widget child;

  ExpandableContainer({
    @required this.child,
    this.collapsedHeight = 0.0,
    this.expandedHeight = 300.0,
    this.expanded = true,
  });

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    return new AnimatedContainer(
      duration: new Duration(milliseconds: 500),
      curve: Curves.easeInOut,
      width: screenWidth,
      height: expanded ? expandedHeight : collapsedHeight,
      child: new Container(
        child: child,
        decoration: new BoxDecoration(border: new Border.all(width: 1.0, color: Colors.blue)),
      ),
    );
  }
}
Net answered 25/5, 2018 at 16:17 Comment(3)
Can the nested list view be made non-scrollable as I just want to render a static list inside each expansion panelSwitzerland
Yes, you can just use Column in ExpandableContainer and adjust the expandedHeight . But as I thought you needed ListView in a nested list I suggest above code and for your case, you can use ExpansionTile.Net
@AjayKumar I dont want fixed expandedHeight, I want it should expand based on nested ListView size and show all items of nested list, is this possible with this code?Odin
S
22

Screenshot:

enter image description here


Code:

class MyPage extends StatelessWidget {
  List<Widget> _getChildren(int count, String name) => List<Widget>.generate(
        count,
        (i) => ListTile(title: Text('$name$i')),
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: [
          ExpansionTile(
            title: Text('List-A'),
            children: _getChildren(4, 'A-'),
          ),
          ExpansionTile(
            title: Text('List-B'),
            children: _getChildren(3, 'B-'),
          ),
        ],
      ),
    );
  }
}
Syntax answered 30/12, 2020 at 10:4 Comment(1)
ExpansionTile gets automatically collaps when there's long list. #69253825Rabin

© 2022 - 2024 — McMap. All rights reserved.