Listview group by date Dart
Asked Answered
M

6

15

I've been trying to get some messages ordered by date, but I'm not getting it working. I have tried different packages like grouped_list and sticky_headers.

With sticky_headers I managed to get a header above every post but that's not what I want. I want to have the messages sorted by day. Like the image below:
Application example

Below I have an dataset with de data I get from my API. My intention is to get 40 messages from the API. Those 40 messages are sorted by time only the messages must be grouped by day in the app as the image above.

Edit: After some playing with sticky_header package on flutter. I have now managed to get the headers. Only do not manage to get the messages under the correct header.

Dataset:

[
  {
    "time": "2020-06-16T10:31:12.000Z",
    "message": "P2 BGM-01 HV buiten materieel (Gas lekkage) Franckstraat Arnhem 073631"
  },
  {
    "time": "2020-06-16T10:29:35.000Z",
    "message": "A1 Brahmslaan 3862TD Nijkerk 73278"
  },
  {
    "time": "2020-06-16T10:29:35.000Z",
    "message": "A2 NS Station Rheden Dr. Langemijerweg 6991EV Rheden 73286"
  },
  {
    "time": "2020-06-15T09:41:18.000Z",
    "message": "A2 VWS Utrechtseweg 6871DR Renkum 74636"
  },
  {
    "time": "2020-06-14T09:40:58.000Z",
    "message": "B2 5623EJ : Michelangelolaan Eindhoven Obj: ziekenhuizen 8610 Ca CATH route 522 PAAZ Rit: 66570"
  }
]
Metrology answered 23/6, 2020 at 8:47 Comment(0)
B
28

First, breakdown your code to get group by date map from your dataset. Then use this map to construct the UI.

Use collection package to use groupBy function.

import "package:collection/collection.dart";

void main() {
  final dataSet = [
    {
      "time": "2020-06-16T10:31:12.000Z",
      "message": "Message1",
    },
    {
      "time": "2020-06-16T10:29:35.000Z",
      "message": "Message2",
    },
    {
      "time": "2020-06-15T09:41:18.000Z",
      "message": "Message3",
    },
  ];

  var groupByDate = groupBy(dataSet, (obj) => obj['time'].substring(0, 10));
  groupByDate.forEach((date, list) {
    // Header
    print('${date}:');

    // Group
    list.forEach((listItem) {
      // List item
      print('${listItem["time"]}, ${listItem["message"]}');
    });
    // day section divider
    print('\n');
  });
}

Output:

2020-06-16:
2020-06-16T10:31:12.000Z, Message1
2020-06-16T10:29:35.000Z, Message2


2020-06-15:
2020-06-15T09:41:18.000Z, Message3

I hope this snippet will help you to start constructing your UI. You can use ListView widget for list items in each group.

Go through DateTime, DateFormat classes to handle date, time values.

Brotherly answered 23/6, 2020 at 12:59 Comment(0)
I
28

You should be able to achieve that by some custom logic on date. Check if two consecutive Dates are same if not add a Text Header in between those list items.

Heres the implementation of above approach.

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

void main() {
  final dateString = '2020-06-16T10:31:12.000Z';
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 
  List<Map> list = [
    {
      "time": "2020-06-16T10:31:12.000Z",
      "message":
          "P2 BGM-01 HV buiten materieel (Gas lekkage) Franckstraat Arnhem 073631"
    },
    {
      "time": "2020-06-16T10:29:35.000Z",
      "message": "A1 Brahmslaan 3862TD Nijkerk 73278"
    },
    {
      "time": "2020-06-16T10:29:35.000Z",
      "message": "A2 NS Station Rheden Dr. Langemijerweg 6991EV Rheden 73286"
    },
    {
      "time": "2020-06-15T09:41:18.000Z",
      "message": "A2 VWS Utrechtseweg 6871DR Renkum 74636"
    },
    {
      "time": "2020-06-14T09:40:58.000Z",
      "message":
          "B2 5623EJ : Michelangelolaan Eindhoven Obj: ziekenhuizen 8610 Ca CATH route 522 PAAZ Rit: 66570"
    }
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ListView.builder(
            itemCount: list.length,
            itemBuilder: (_, index) {
              bool isSameDate = true;
              final String dateString = list[index]['time'];
              final DateTime date = DateTime.parse(dateString);
              final item = list[index];
              if (index == 0) {
                isSameDate = false;
              } else {
                final String prevDateString = list[index - 1]['time'];
                final DateTime prevDate = DateTime.parse(prevDateString);
                isSameDate = date.isSameDate(prevDate);
              }
              if (index == 0 || !(isSameDate)) {
                return Column(children: [
                  Text(date.formatDate()),
                  ListTile(title: Text('item $index'))
                ]);
              } else {
                return ListTile(title: Text('item $index'));
              }
            }),
      ),
    );
  }
}

const String dateFormatter = 'MMMM dd, y';

extension DateHelper on DateTime {
  
   String formatDate() {
     final formatter = DateFormat(dateFormatter);
      return formatter.format(this);
  }
  bool isSameDate(DateTime other) {
    return this.year == other.year &&
        this.month == other.month &&
        this.day == other.day;
  }

  int getDifferenceInDaysWithNow() {
    final now = DateTime.now();
    return now.difference(this).inDays;
  }
}

Output

enter image description here

Immunoreaction answered 9/4, 2022 at 5:27 Comment(6)
I find this answer better for its succinct nature and not depending on another package.Afternoons
what is your approach if the list is reverse?Margarettmargaretta
How about setting reverse: true in Listview.builder?Immunoreaction
I was facing problems with GroupedListView indexedItemBuilder, this implementation works great for my use case! ThanksPetree
reverse: true does not WORK ☹️Barograph
You can always modify/sort the list before rendering.Immunoreaction
C
9

There are some dart.pub packages called sticky_grouped_list and grouped_list (mentioned in your question). They have done a good work around group elements by a common property. I have tried sticky_grouped_list and besides I don't have used in your specific scenario it's seems to apply to it, once the Lists can group elements by a dynamic type.

Beyond the appearance, the main functional difference between your code and that example is they have used a DateTime element instead a String as a better manner to group the list. The sticky group list view is interesting due the fact the current date stick on the top of screen when it's rolled down.

enter image description here

instead of using a ListView create a GroupedListView Widget:

  GroupedListView<dynamic, String>(
    elements: _elements,
    groupBy: (element) => element['group'],
    groupSeparatorBuilder: (String groupByValue) => Text(groupByValue),
    itemBuilder: (context, dynamic element) => Text(element['name']),
    order: GroupedListOrder.ASC,
  ),

With the StickyGroupedListView you also can group by a custom object:

StickyGroupedListView<CustomObject, DateTime>(
  elements: pedidos,
  order: StickyGroupedListOrder.ASC,
  groupBy: (CustomObject element) => DateTime(
      element.dateGroup.year,
      element.dateGroup.month,
      element.dateGroup.day),
  groupSeparatorBuilder: (CustomObject element) =>
      renderDataCabecalho(element),
  itemBuilder: (context, element) {
    return _buildPedidoItemView(element, context);
  },
),

If it don't apply to your needs may you could get some insights on their approach to group elements by.

Cordovan answered 15/7, 2020 at 12:58 Comment(3)
It keep recreate widget when add itemsBerger
@Berger It would depends on how you add the itens. Try to do it with RXDart (BehaviorSubject and StreamBuilder) it would be more sophisticated to control it.Cordovan
@Berger good to know you have succeed!Cordovan
S
2

If the API supports sorting, obtaining results in the correct order will be the simplest, and most high-performance solution.

Syringomyelia answered 23/6, 2020 at 9:0 Comment(8)
The API doesn't support it. Then I would have to build it into the API myself. Only then is the question how I get those headers working.. Thx for your answer!Metrology
In that case, does the database query (issued from within the API) support sorting? That will be easiest and most performant solution.Syringomyelia
Yes the query can be sorted by date. But then I will have to check when I have to show a new header dateMetrology
Sorry, I've just read your question again and I think your phrasing threw me. So (just making sure I have this straight) what you really want to know is not how have the messages SORTED by day but how to have the messages GROUPED by day. Correct?Syringomyelia
Haha, doesn't matter. But that's exactly what I want to know!Metrology
Cool. OK. The screenshot is showing only results from one day. Will your app also only be displaying one day's messages on your page, or will you be showing multiple days messages on a page? If the former, then you could save network traffic, and device memory, by only requesting one day's records at a time. Then only requesting another day's records when the user requests them (by whatever means)Syringomyelia
Check the post, added the info!Metrology
Let us continue this discussion in chat.Metrology
C
0
I think you should filter data in the model to create groups follow the date.
class ListDate {
  int id;
  String name;  
  String date;
  List<DateItem> results;
}
class DateItem {
  String id;
  String date;
  String name;
}
After parse JSON to model, you can run listView by ListDate().results
Continent answered 23/6, 2020 at 9:10 Comment(0)
N
0

You can group objects by date (start of day for example, considering your example) and then show whatever you want (without libs).

Example:

1 - We have some class with dates to group by:

class WalletAccountRecord {
  String id;
  String name;
  DateTime date;
....

2 - We have list with such items (in our case coming from api, but any building way is ok for approach understanding)

List<WalletAccountRecord> records = response.records;

3 - Then we group this list by dates (by start of day)

Map<Jiffy, List<WalletAccountRecord>> recordsByDays = {};
for (var record in records) {
  Jiffy date = Jiffy.parseFromDateTime(record.date).startOf(Unit.day);
  recordsByDays.update(date, (List<WalletAccountRecord> value) => [record, ...value], ifAbsent: () => [record]);
}
_logger.d('Records by Dates: ${recordsByDays.length} days -> ${recordsByDays.keys}');

4 - now, considering all the data is grouped into Map we can display it in wanted way. Full example with our UI:

 class WalletRecords extends StatelessWidget {
  final AnalyticsService _analyticsService = Get.find();
  final WalletRecordsStateController _walletRecordsStateController = Get.find();
  final Logger _logger = LoggerHelper.logger();

  @override
  Widget build(BuildContext context) {
    return Obx(() => _recordsItems());
  }

  Widget _recordsItems() {
    RecordsSearchResultRx recordsRx = _walletRecordsStateController.recordsGet();
    //TODO Revision flow. if no wallet (when??) then just show 'no records yet'
    //TODO no wallet/loading handle

    List<WalletAccountRecord> records = recordsRx.records;

    Map<Jiffy, List<WalletAccountRecord>> recordsByDays = {};
    for (var record in records) {
      Jiffy date = Jiffy.parseFromDateTime(record.date).startOf(Unit.day);
      recordsByDays.update(date, (List<WalletAccountRecord> value) => [record, ...value], ifAbsent: () => [record]);
    }
    _logger.d('Records by Dates: ${recordsByDays.length} days -> ${recordsByDays.keys}');

    return _recordsByDates(recordsByDays);
  }

  Widget _recordsByDates(Map<Jiffy, List<WalletAccountRecord>> recordsByDays) {
    return ListView.builder(
      itemCount: recordsByDays.length,
      shrinkWrap: true,
      itemBuilder: (context, int index) {
        var date = recordsByDays.keys.elementAt(index);
        return _recordsByDate(date, recordsByDays[date] ?? []);
      },
    );
  }

  Widget _recordsByDate(Jiffy date, List<WalletAccountRecord> recordsByDay) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
      child: Container(
        decoration: BoxDecoration(
            color: UiConstants.colorPrimaryDark.withOpacity(0.3),
            border: Border.all(color: Colors.grey),
            borderRadius: const BorderRadius.all(Radius.circular(15))),
        child: Column(children: [
          Padding(
            padding: const EdgeInsets.only(left: 20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                    child: Text(date.format(pattern: 'd.MM.yy'),
                        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600))),
                Wrap(children: [
                  IconButton(icon: const Icon(LineIcons.plus), onPressed: () => {}),
                  IconButton(icon: const Icon(LineIcons.pen), onPressed: () => {}),
                ]),
              ],
            ),
          ),
          const Divider(height: 5, indent: 10, endIndent: 10),
          _recordItems(recordsByDay),
        ]),
      ),
    );
  }

  Widget _recordItems(List<WalletAccountRecord> records) {
    return ListView.separated(
      itemCount: records.length,
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      separatorBuilder: (BuildContext context, int index) => const Divider(height: 1, indent: 10, endIndent: 10),
      itemBuilder: (context, index) {
        return _recordOverview(records[index]);
      },
    );
  }

  Widget _recordOverview(WalletAccountRecord record) {
    return ListTile(
      dense: true,
      horizontalTitleGap: 0,
      title: Text(record.category!, style: const TextStyle(fontWeight: FontWeight.bold)),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(record.account.accountName),
          Text(record.operationType.name),
          Text(record.note ?? ''),
        ],
      ),
      contentPadding: const EdgeInsets.only(left: 3),
      trailing: Text('${record.amount.amount} ${record.amount.currencyCode}'),
      //todo dynamic based on account color per account
      leading: Container(color: Colors.blue, width: 5, padding: EdgeInsets.zero, margin: EdgeInsets.zero),
    );
  }
}

ui by dates

Natural answered 27/1 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.