skip to Main Content

I currently have a ListView that displays cards that have several properties and it’s being displayed properly within an expanded widget:

    return Expanded(
       child: renderCardsList(docs, homeState),
     );

Each of these cards have dates and I want to split the list by the current date (to have something like past/upcoming), but I’m having issues displaying 2 ListViews with text in between.

For example, ideally I’d like something like this:

            child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            children: [
              Text('Upcoming'),
              renderCardsList(docs1, homeState),
              Text('Past'),
              renderCardsList(docs2, homeState),
            ],
          ),
        ),

With the entire list being scrollable. I’ve tried several combinations of adding Expanded widgets, switching columns up, etc with no luck.

Any help is much appreciated, thanks!

Code for renderCardsList

  ListView renderCardsList(dynamic list, dynamic state) {
ListView result = ListView.builder(
  itemCount: list.length,
  itemBuilder: (context, index) {
    
    DocumentSnapshot? snapshot = list[index];

    if (snapshot != null) {
      var snapshotData = snapshot.data() as Map<String, dynamic>

      Object obj = Object(
        id: key,
        name: snapshotData['name'],
        date: snapshotData[‘date’],
      );

        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
          child: Card(
              data: obj,
        );
      }
    }

    return const SizedBox.shrink();
  },
);

return result;

}

2

Answers


  1. Chosen as BEST ANSWER

    Update: I didn't figure this out for a ListView but I was able to rewrite my renderCardsList function to return List instead and return those cards through that.

    Then you can use them just like this.

          child: Column(
            children: [
              Text('Upcoming'),
              ...renderCardsList(docs1, homeState),
              Text('Past'),
              ...renderCardsList(docs2, homeState),
            ],
          ),
    

    This works for me for now as it's not a large list but would be curious if anyone has a way to use the ListView as I believe it's much more performant for long lists.


  2. You are encountering an issue with the SingleChildScrollView that contains ListViews.

    A ListView will attempt to take as much space as possible for its axis. In your case, the vertical axis. However, the vertical axis is unbounded, because a SingleChildScrollView has an infinite height.

    The solution is simply to tell your ListViews to only take up as much space as possible:

    ListView renderCardsList(List<MyObject> list) {
      return ListView.builder(
        /// When 'shrinkWrap' is set to true, the list will only be as big as its
        /// children, instead of attempting to fill the available space.
        shrinkWrap: true, // <=== RIGHT HERE!
        itemCount: list.length,
        itemBuilder: (context, index) => Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 5.0),
          child: MyCard(object: list[index]),
        ),
      );
    }
    

    Without shrinkWrap being set to true, the framework will not be able to lay out your list views in an unbounded vertical axis.

    I created mock data and put multiple ListViews inside a Column, inside a SingleChildScrollView:

    return SingleChildScrollView(
      primary: false,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (elementsFromThePast.isNotEmpty) ...[
            const Text("Past"),
            renderCardsList(elementsFromThePast),
          ],
          if (elementsForToday.isNotEmpty) ...[
            const Text("Today"),
            renderCardsList(elementsForToday),
          ],
          if (elementsFromTheFuture.isNotEmpty) ...[
            const Text("Future"),
            renderCardsList(elementsFromTheFuture),
          ],
        ],
      ),
    );
    

    Output using shrinkWrap:

    layout output

    Here is the full example code:

    extension DateTimeExtension on DateTime {
      DateTime removeTimeComponents() {
        return DateTime(year, month, day, 0, 0, 0);
      }
    }
    
    class MyGroupedListViews extends StatelessWidget {
      const MyGroupedListViews({super.key});
    
      @override
      Widget build(BuildContext context) {
        final list = getMockList();
        final today = DateTime.now().removeTimeComponents();
    
        final elementsFromThePast = list
            .where((element) => element.date.isBefore(today))
            .toList(growable: false);
    
        final elementsForToday =
            list.where((element) => element.date == today).toList(growable: false);
    
        final elementsFromTheFuture = list
            .where((element) => element.date.isAfter(today))
            .toList(growable: false);
    
        return SingleChildScrollView(
          primary: false,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (elementsFromThePast.isNotEmpty) ...[
                const Text("Past"),
                renderCardsList(elementsFromThePast),
              ],
              if (elementsForToday.isNotEmpty) ...[
                const Text("Today"),
                renderCardsList(elementsForToday),
              ],
              if (elementsFromTheFuture.isNotEmpty) ...[
                const Text("Future"),
                renderCardsList(elementsFromTheFuture),
              ],
            ],
          ),
        );
      }
    }
    
    /// Mock model.
    class MyObject {
      const MyObject({required this.id, required this.name, required this.date});
    
      final int id;
      final String name;
      final DateTime date;
    }
    
    List<MyObject> getMockList() {
      final today = DateTime.now().removeTimeComponents();
      final yesterday = today.subtract(const Duration(days: 1));
      final tomorrow = today.add(const Duration(days: 1));
    
      return [
        MyObject(id: 0, name: "1", date: yesterday),
        MyObject(id: 1, name: "2", date: today),
        MyObject(id: 2, name: "3", date: yesterday),
        MyObject(id: 3, name: "4", date: tomorrow),
        MyObject(id: 4, name: "5", date: today),
        MyObject(id: 5, name: "6", date: tomorrow),
        MyObject(id: 6, name: "7", date: tomorrow),
      ];
    }
    
    /// Mock card to display [MyObject].
    class MyCard extends StatelessWidget {
      const MyCard({super.key, required this.object});
    
      final MyObject object;
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            Expanded(flex: 1, child: Text(object.name)),
            Expanded(flex: 3, child: Text(object.date.toString())),
          ],
        );
      }
    }
    
    /// Mock list view to display [MyCard]s.
    ///
    /// Mock data will be used to populate the list, you can replace it with your
    /// own.
    ListView renderCardsList(List<MyObject> list) {
      return ListView.builder(
        /// When 'shrinkWrap' is set to true, the list will only be as big as its
        /// children, instead of attempting to fill the available space.
        shrinkWrap: true,
        itemCount: list.length,
        itemBuilder: (context, index) => Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 5.0),
          child: MyCard(object: list[index]),
        ),
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search