infinite_grouped_list 1.3.1
infinite_grouped_list: ^1.3.1 copied to clipboard
Dynamic scrolling list in Flutter, efficiently grouping items and seamlessly loading more data as user scrolls
Show some love by dropping a ⭐ at GitHub
Infinite Grouped List #
Brings together infinite scrolling, group-based item organization, and numerous other enhancements to improve the end-user experience.
Key Features #
-
Infinite Scrolling: The widget supports loading more data as the user reaches the end of the list. This is essential for handling large datasets without overwhelming the user or their device.
-
Grouping of Items: The widget can organize items into groups based on user-defined criteria. This helps to make sense of large amounts of data by breaking it down into manageable chunks.
-
Reactive State Management: 🆕 Full support for modern state management patterns like BLoC, Provider, and Riverpod with dedicated reactive constructors that separate event triggering from data listening.
-
Customizable Loading and Error States: You can provide custom widgets to be displayed while data is being loaded or if an error occurs. This allows for a seamless, branded experience.
-
Pull-to-Refresh: The widget incorporates a pull-to-refresh feature, letting users manually trigger a refresh of the list's content.
-
Sticky Group Headers: Headers stick to the top of the list as the user scrolls, making it easier to understand the context of the data they're viewing. Can be changed.
-
Group Anchoring: Opt-in jump-to-group support lets you instantly scroll to any group (for example "Today") through the controller without impacting base performance.
Usage #
The InfiniteGroupedList offers two usage patterns to fit different architectural approaches:
🔄 Reactive Pattern (Recommended for Modern Apps)
Perfect for apps using BLoC, Provider, Riverpod, or any external state management:
BlocBuilder<ItemsBloc, ItemsState>(
builder: (context, state) {
return InfiniteGroupedList<Item, String, String>.reactive(
// External state from your state management solution
items: state.items,
isLoading: state.isLoading,
hasReachedMax: state.hasReachedMax,
error: state.error,
// Event trigger - cleanly separated from data fetching
onLoadMoreTriggered: () {
context.read<ItemsBloc>().add(LoadMoreItems());
},
// Refresh trigger
onRefresh: () {
context.read<ItemsBloc>().add(RefreshItems());
},
// UI builders
itemBuilder: (item) => ListTile(title: Text(item.name)),
groupBy: (item) => item.category,
groupCreator: (category) => category,
groupTitleBuilder: (title, _, __, ___) => Text(title),
);
},
)
⚡ Imperative Pattern (Traditional Approach)
For apps that prefer direct data fetching within the widget:
InfiniteGroupedList(
onLoadMore: (paginationInfo) async {
// Fetch data directly and return it
return await apiService.fetchItems(
page: paginationInfo.page,
limit: 20,
);
},
itemBuilder: (item) => ListTile(title: Text(item.name)),
groupBy: (item) => item.category,
groupCreator: (category) => category,
groupTitleBuilder: (title, _, __, ___) => Text(title),
)
Key Differences
- Reactive: Event triggering and data listening are completely separated. Your state management handles data fetching, and the widget displays the current state.
- Imperative: The widget directly calls your data fetching function and manages the loading states internally.
PaginationInfo Helper
When using the imperative pattern, PaginationInfo provides pagination context:
offset: Current item offset for offset-based paginationpage: Current page number for page-based paginationlimit: Items per page (configurable via controller)
The InfiniteGroupedList widget is a comprehensive solution for any use case that involves displaying large amounts of data in an organized, easy-to-navigate manner.
Jumping to a Group #
Need to anchor the list to a specific group (e.g. "Today")? Enable anchoring on the widget and invoke the new controller helper:
final controller = InfiniteGroupedListController<Transaction, DateTime, String>();
InfiniteGroupedList(
enableAnchoring: true,
controller: controller,
// ... other params
);
// Later, jump to a concrete title or resolve it dynamically with a predicate
// Option A: jump by the exact group title you already know
controller.jumpToGroup(
title: 'Today',
alignment: 0.0, // pin the header to the top
);
// Option B: resolve dynamically via predicate (provide predicate *instead* of title)
controller.jumpToGroup(
predicate: (title, groupBy) => isSameDay(groupBy, DateTime.now()),
alignment: 0.0,
loadUntilFound: true, // imperative mode only
);
Anchoring is opt-in, so you only incur the tiny bookkeeping cost when you actually need the feature. The optional loadUntilFound flag is available in imperative mode; reactive consumers should trigger their own data loads before retrying the jump.
Examples #
Explore comprehensive examples demonstrating different usage patterns:
-
🆕 Reactive BLoC Example: Complete implementation using reactive pattern with flutter_bloc, including error handling, loading states, and event-driven architecture.
-
🎯 Jump to Group Example: Demonstrates enabling anchoring plus
controller.jumpToGroup()to instantly scroll to the "Today" section. -
📅 Group by Date: Traditional imperative pattern grouping transactions by date with custom group titles.
-
🏷️ Group by Type: Demonstrates grouping items by category/type with different visual treatments.
-
🔲 Grid Layout: Shows how to use the
.gridView()constructor for grid-based layouts.
Run the example app to see all patterns in action with an interactive example selection screen.
Migration Guide #
Existing users: Your current code continues to work without any changes! The new reactive constructors are purely additive.
Moving to reactive pattern:
- Replace
InfiniteGroupedList()withInfiniteGroupedList.reactive() - Move your
onLoadMorelogic to your state management solution - Replace the
onLoadMoreparameter withonLoadMoreTriggeredcallback - Provide external state via
items,isLoading,hasReachedMaxparameters
The reactive pattern is recommended for new projects using modern state management, while the imperative pattern remains fully supported for simpler use cases.
