EdsonBueno / infinite_scroll_pagination

Flutter package to help you lazily load and display pages of items as the user scrolls down your screen.
https://pub.dev/packages/infinite_scroll_pagination
MIT License
612 stars 202 forks source link

Grouping items in a list? #264

Closed davoutuk closed 5 months ago

davoutuk commented 1 year ago

Any suggestions on how I can introduce a grouping display of items in an infinite list?

By grouping, I mean adding a title to a sublist of items, as shown by the 'language' grouping in the screenshot below

image

NALAWALAMURTUZA commented 1 year ago

I face the same issue.

Screenshot 2023-04-27 at 11 28 47 AM

but after grouping when I add a list of groups using this method( appendPage) method. it shows multiple times.

NALAWALAMURTUZA commented 1 year ago

Resolved.

instead of using appendPage i set pagingController.value. below is a screenshot.

Screenshot 2023-04-27 at 12 01 38 PM

Group Method

  List<VisaItemHeader> group({required List<VisaItem> data}) {
    List<VisaItemHeader> list = [];
    Map<String, List<VisaItem>> map =
        groupBy(data, (VisaItem obj) => obj.visaGroupId ?? "");
    map.forEach((key, value) {
      list.add(VisaItemHeader(key, value));
    });
    return list;
  }
esentis commented 1 year ago

@davoutuk have you found any possible solution ?

DavidOrakpo commented 1 year ago

Resolved.

instead of using appendPage i set pagingController.value. below is a screenshot.

Screenshot 2023-04-27 at 12 01 38 PM

Group Method

  List<VisaItemHeader> group({required List<VisaItem> data}) {
    List<VisaItemHeader> list = [];
    Map<String, List<VisaItem>> map =
        groupBy(data, (VisaItem obj) => obj.visaGroupId ?? "");
    map.forEach((key, value) {
      list.add(VisaItemHeader(key, value));
    });
    return list;
  }

This replaces the previous page with the new page group... Which isn't what we want

DavidOrakpo commented 1 year ago

I've found a solution making use of @NALAWALAMURTUZA grouping method. I modified it to fit my code below.

List<TransactionItemHeader> group({required List<Datum> data}) {
    List<TransactionItemHeader> list = [];
    Map<String, List<Datum>> map = groupBy(
        data,
        (Datum obj) => Validators.dateTimeToString(DateTime(
            obj.pubDate!.year, obj.pubDate!.month, obj.pubDate!.day))!);
    map.forEach((key, value) {
      list.add(TransactionItemHeader(headerTitle: key, transactionData: value));
    });
    return list;
  }

I did not make use of PageController.value as he did however. Doing that causes the whole page history to be replaced, which is not what I want in a scrolling view.

What I did to stop the duplication of a group, is this:

Future<void> fetchPaginationPage({required int pageKey}) async {
    const pageSize = 20;
    try {
      final transactionsList = await fetchAllTransactionsPagination(
          authToken: getIt<UserDependencies>().getAuthToken()!,
          merchantTribeRef: getIt<UserDependencies>().getMerchantTribeRef()!,
          startDate: DateTime(2008),
          endDate: DateTime(
              DateTime.now().year, DateTime.now().month, DateTime.now().day),
          transactionType: TransactionType.AllFiat,
          pageNumber: pageKey);

      final isLastPage = transactionsList.length < pageSize;

      final transactionItemsSoFar = pagingController.itemList;
      final groupedTransactionList = group(data: transactionsList);
      if (transactionItemsSoFar != null) {
        TransactionItemHeader? temp;
        for (var groupedElement in groupedTransactionList) {
          temp = transactionItemsSoFar.firstWhereOrNull(
              (element) => element.headerTitle == groupedElement.headerTitle);
          if (temp != null) {
            break;
          }
        }

        if (temp != null) {
          var temp2 = groupedTransactionList.firstWhereOrNull(
              (element) => temp!.headerTitle == element.headerTitle);
          if (temp2 != null) {
            var newTransactionItemHeader = TransactionItemHeader(
                headerTitle: temp2.headerTitle,
                transactionData: [
                  ...temp.transactionData,
                  ...temp2.transactionData
                ]);
            var tempIndex = transactionItemsSoFar.indexWhere((element) =>
                newTransactionItemHeader.headerTitle == element.headerTitle);
            pagingController.itemList![tempIndex] = newTransactionItemHeader;
            groupedTransactionList.removeAt(0);
          }
        }
      }
      // pagingController.
      if (isLastPage) {
        pagingController.appendLastPage(groupedTransactionList);
        // pagingController.value = PagingState(
        //     nextPageKey: null, itemList: group(data: transactionsList));
      } else {
        final nextPageKey = pageKey + 1;
        pagingController.appendPage(groupedTransactionList, nextPageKey);
        // pagingController.value = PagingState(
        //     nextPageKey: nextPageKey, itemList: group(data: transactionsList));
      }
    } catch (e) {
      pagingController.error = e;
    }
  }

So far, it works pretty good and seamless.

clragon commented 11 months ago

for an actual grouped list, I have used the grouped_list package and created a PagedGroupedListView:

import 'package:flutter/material.dart';
import 'package:grouped_list/sliver_grouped_list.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:sliver_tools/sliver_tools.dart';

class PagedGroupedListView<PageKeyType, ItemType, SortType>
    extends BoxScrollView {
  const PagedGroupedListView({
    super.key,
    required this.pagingController,
    required this.builderDelegate,
    this.shrinkWrapFirstPageIndicators = false,
    required this.groupBy,
    this.groupComparator,
    this.groupSeparatorBuilder,
    this.groupHeaderBuilder,
    this.itemComparator,
    this.order = GroupedListOrder.ASC,
    this.sort = true,
    this.separator = const SizedBox.shrink(),
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    super.cacheExtent,
    super.semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  });

  /// Matches [PagedLayoutBuilder.pagingController].
  final PagingController<PageKeyType, ItemType> pagingController;

  /// Matches [PagedLayoutBuilder.builderDelegate].
  final PagedChildBuilderDelegate<ItemType> builderDelegate;

  /// Matches [PagedLayoutBuilder.shrinkWrapFirstPageIndicators].
  final bool shrinkWrapFirstPageIndicators;

  /// Matches [SliverGroupedListView.groupBy].
  final SortType Function(ItemType element) groupBy;

  /// Matches [SliverGroupedListView.groupComparator].
  final int Function(SortType value1, SortType value2)? groupComparator;

  /// Matches [SliverGroupedListView.itemComparator].
  final int Function(ItemType element1, ItemType element2)? itemComparator;

  /// Matches [SliverGroupedListView.groupSeparatorBuilder].
  final Widget Function(SortType value)? groupSeparatorBuilder;

  /// Matches [SliverGroupedListView.groupHeaderBuilder].
  final Widget Function(ItemType element)? groupHeaderBuilder;

  /// Matches [SliverGroupedListView.order].
  final GroupedListOrder order;

  /// Matches [SliverGroupedListView.sort].
  final bool sort;

  /// Matches [SliverGroupedListView.separator].
  final Widget separator;

  @override
  Widget buildChildLayout(BuildContext context) {
    return PagedSliverGroupedListView(
      pagingController: pagingController,
      builderDelegate: builderDelegate,
      shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators,
      groupBy: groupBy,
      groupComparator: groupComparator,
      groupSeparatorBuilder: groupSeparatorBuilder,
      groupHeaderBuilder: groupHeaderBuilder,
      itemComparator: itemComparator,
      order: order,
      sort: sort,
      separator: separator,
    );
  }
}

class PagedSliverGroupedListView<PageKeyType, ItemType, SortType>
    extends StatelessWidget {
  const PagedSliverGroupedListView({
    super.key,
    required this.pagingController,
    required this.builderDelegate,
    this.shrinkWrapFirstPageIndicators = false,
    required this.groupBy,
    this.groupComparator,
    this.groupSeparatorBuilder,
    this.groupHeaderBuilder,
    this.itemComparator,
    this.order = GroupedListOrder.ASC,
    this.sort = true,
    this.separator = const SizedBox.shrink(),
  });

  /// Matches [PagedLayoutBuilder.pagingController].
  final PagingController<PageKeyType, ItemType> pagingController;

  /// Matches [PagedLayoutBuilder.builderDelegate].
  final PagedChildBuilderDelegate<ItemType> builderDelegate;

  /// Matches [PagedLayoutBuilder.shrinkWrapFirstPageIndicators].
  final bool shrinkWrapFirstPageIndicators;

  /// Matches [SliverGroupedListView.groupBy].
  final SortType Function(ItemType element) groupBy;

  /// Matches [SliverGroupedListView.groupComparator].
  final int Function(SortType value1, SortType value2)? groupComparator;

  /// Matches [SliverGroupedListView.itemComparator].
  final int Function(ItemType element1, ItemType element2)? itemComparator;

  /// Matches [SliverGroupedListView.groupSeparatorBuilder].
  final Widget Function(SortType value)? groupSeparatorBuilder;

  /// Matches [SliverGroupedListView.groupHeaderBuilder].
  final Widget Function(ItemType element)? groupHeaderBuilder;

  /// Matches [SliverGroupedListView.order].
  final GroupedListOrder order;

  /// Matches [SliverGroupedListView.sort].
  final bool sort;

  /// Matches [SliverGroupedListView.separator].
  final Widget separator;

  @override
  Widget build(BuildContext context) {
    Widget buildLayout(
      IndexedWidgetBuilder itemBuilder,
      int itemCount, {
      WidgetBuilder? statusIndicatorBuilder,
    }) =>
        MultiSliver(
          children: [
            SliverGroupedListView<ItemType, SortType>(
              key: key,
              elements: pagingController.itemList!,
              groupBy: groupBy,
              groupComparator: groupComparator,
              groupSeparatorBuilder: groupSeparatorBuilder,
              groupHeaderBuilder: groupHeaderBuilder,
              indexedItemBuilder: (context, item, index) =>
                  itemBuilder(context, index),
              itemComparator: itemComparator,
              order: order,
              sort: sort,
              separator: separator,
            ),
            if (statusIndicatorBuilder != null)
              SliverToBoxAdapter(
                child: statusIndicatorBuilder(context),
              )
          ],
        );

    return PagedLayoutBuilder<PageKeyType, ItemType>(
      layoutProtocol: PagedLayoutProtocol.sliver,
      pagingController: pagingController,
      builderDelegate: builderDelegate,
      shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators,
      completedListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        noMoreItemsIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: noMoreItemsIndicatorBuilder,
      ),
      loadingListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        progressIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: progressIndicatorBuilder,
      ),
      errorListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        errorIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: errorIndicatorBuilder,
      ),
    );
  }
}
rana1m commented 9 months ago

Hi @clragon , Could you please show a usage example?

clragon commented 9 months ago

You can read how to use the grouped list on the grouped list package page: https://pub.dev/packages/grouped_list

DavidOrakpo commented 3 months ago

for an actual grouped list, I have used the grouped_list package and created a PagedGroupedListView:

import 'package:flutter/material.dart';
import 'package:grouped_list/sliver_grouped_list.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:sliver_tools/sliver_tools.dart';

class PagedGroupedListView<PageKeyType, ItemType, SortType>
    extends BoxScrollView {
  const PagedGroupedListView({
    super.key,
    required this.pagingController,
    required this.builderDelegate,
    this.shrinkWrapFirstPageIndicators = false,
    required this.groupBy,
    this.groupComparator,
    this.groupSeparatorBuilder,
    this.groupHeaderBuilder,
    this.itemComparator,
    this.order = GroupedListOrder.ASC,
    this.sort = true,
    this.separator = const SizedBox.shrink(),
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    super.cacheExtent,
    super.semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  });

  /// Matches [PagedLayoutBuilder.pagingController].
  final PagingController<PageKeyType, ItemType> pagingController;

  /// Matches [PagedLayoutBuilder.builderDelegate].
  final PagedChildBuilderDelegate<ItemType> builderDelegate;

  /// Matches [PagedLayoutBuilder.shrinkWrapFirstPageIndicators].
  final bool shrinkWrapFirstPageIndicators;

  /// Matches [SliverGroupedListView.groupBy].
  final SortType Function(ItemType element) groupBy;

  /// Matches [SliverGroupedListView.groupComparator].
  final int Function(SortType value1, SortType value2)? groupComparator;

  /// Matches [SliverGroupedListView.itemComparator].
  final int Function(ItemType element1, ItemType element2)? itemComparator;

  /// Matches [SliverGroupedListView.groupSeparatorBuilder].
  final Widget Function(SortType value)? groupSeparatorBuilder;

  /// Matches [SliverGroupedListView.groupHeaderBuilder].
  final Widget Function(ItemType element)? groupHeaderBuilder;

  /// Matches [SliverGroupedListView.order].
  final GroupedListOrder order;

  /// Matches [SliverGroupedListView.sort].
  final bool sort;

  /// Matches [SliverGroupedListView.separator].
  final Widget separator;

  @override
  Widget buildChildLayout(BuildContext context) {
    return PagedSliverGroupedListView(
      pagingController: pagingController,
      builderDelegate: builderDelegate,
      shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators,
      groupBy: groupBy,
      groupComparator: groupComparator,
      groupSeparatorBuilder: groupSeparatorBuilder,
      groupHeaderBuilder: groupHeaderBuilder,
      itemComparator: itemComparator,
      order: order,
      sort: sort,
      separator: separator,
    );
  }
}

class PagedSliverGroupedListView<PageKeyType, ItemType, SortType>
    extends StatelessWidget {
  const PagedSliverGroupedListView({
    super.key,
    required this.pagingController,
    required this.builderDelegate,
    this.shrinkWrapFirstPageIndicators = false,
    required this.groupBy,
    this.groupComparator,
    this.groupSeparatorBuilder,
    this.groupHeaderBuilder,
    this.itemComparator,
    this.order = GroupedListOrder.ASC,
    this.sort = true,
    this.separator = const SizedBox.shrink(),
  });

  /// Matches [PagedLayoutBuilder.pagingController].
  final PagingController<PageKeyType, ItemType> pagingController;

  /// Matches [PagedLayoutBuilder.builderDelegate].
  final PagedChildBuilderDelegate<ItemType> builderDelegate;

  /// Matches [PagedLayoutBuilder.shrinkWrapFirstPageIndicators].
  final bool shrinkWrapFirstPageIndicators;

  /// Matches [SliverGroupedListView.groupBy].
  final SortType Function(ItemType element) groupBy;

  /// Matches [SliverGroupedListView.groupComparator].
  final int Function(SortType value1, SortType value2)? groupComparator;

  /// Matches [SliverGroupedListView.itemComparator].
  final int Function(ItemType element1, ItemType element2)? itemComparator;

  /// Matches [SliverGroupedListView.groupSeparatorBuilder].
  final Widget Function(SortType value)? groupSeparatorBuilder;

  /// Matches [SliverGroupedListView.groupHeaderBuilder].
  final Widget Function(ItemType element)? groupHeaderBuilder;

  /// Matches [SliverGroupedListView.order].
  final GroupedListOrder order;

  /// Matches [SliverGroupedListView.sort].
  final bool sort;

  /// Matches [SliverGroupedListView.separator].
  final Widget separator;

  @override
  Widget build(BuildContext context) {
    Widget buildLayout(
      IndexedWidgetBuilder itemBuilder,
      int itemCount, {
      WidgetBuilder? statusIndicatorBuilder,
    }) =>
        MultiSliver(
          children: [
            SliverGroupedListView<ItemType, SortType>(
              key: key,
              elements: pagingController.itemList!,
              groupBy: groupBy,
              groupComparator: groupComparator,
              groupSeparatorBuilder: groupSeparatorBuilder,
              groupHeaderBuilder: groupHeaderBuilder,
              indexedItemBuilder: (context, item, index) =>
                  itemBuilder(context, index),
              itemComparator: itemComparator,
              order: order,
              sort: sort,
              separator: separator,
            ),
            if (statusIndicatorBuilder != null)
              SliverToBoxAdapter(
                child: statusIndicatorBuilder(context),
              )
          ],
        );

    return PagedLayoutBuilder<PageKeyType, ItemType>(
      layoutProtocol: PagedLayoutProtocol.sliver,
      pagingController: pagingController,
      builderDelegate: builderDelegate,
      shrinkWrapFirstPageIndicators: shrinkWrapFirstPageIndicators,
      completedListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        noMoreItemsIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: noMoreItemsIndicatorBuilder,
      ),
      loadingListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        progressIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: progressIndicatorBuilder,
      ),
      errorListingBuilder: (
        context,
        itemBuilder,
        itemCount,
        errorIndicatorBuilder,
      ) =>
          buildLayout(
        itemBuilder,
        itemCount,
        statusIndicatorBuilder: errorIndicatorBuilder,
      ),
    );
  }
}

This doesn't work at all. The groupBy function of the list view, should expose a single item of the list as a parameter. Eg: if I have a List of String, the group by should expose a single string item. The code above, exposes the entire list of string which breaks the functionality sadly