diff --git a/README.md b/README.md
index b160503..ef7047a 100644
--- a/README.md
+++ b/README.md
@@ -45,21 +45,21 @@ import 'package:grouped_list/grouped_list.dart';
```
### Parameters:
-| Name | Description | Required | Default value |
-|----|----|----|----|
-|`elements`| A list of the data you want to display in the list | required | - |
-|`groupBy` |Function which maps an element to its grouped value | required | - |
-|`itemBuilder` / `indexedItemBuilder` / `interdependentItemBuilder`| Function which returns an Widget which defines the item. `indexedItemBuilder` provides the current index as well. `interdependentItemBuilder` provides the previous and next items as well.`indexedItemBuilder` is preferred over `interdependentItemBuilder` and `interdependentItemBuilder` is preferred over`itemBuilder` | yes, either of them | - |
-|`groupSeparatorBuilder` / `groupHeaderBuilder`| Function which returns a Widget which defines the group headers. While `groupSeparatorBuilder` gets the `groupBy`-value as parameter `groupHeaderBuilder` gets the whole element. If both are defined `groupHeaderBuilder` is preferred| yes, either of them | - |
-|`groupStickyHeaderBuilder` | Function which returns a Widget which defines the sticky group header, when `useStickyGroupSeparators` is `true`. If not defined `groupSeparatorBuilder` or `groupHeaderBuilder` will be used as described above. | no | - |
-|`useStickyGroupSeparators` | When set to true the group header of the current visible group will stick on top | no | `false` |
-|`floatingHeader` | Whether the sticky group header float over the list or occupy it's own space | no | `false` |
-|`stickyHeaderBackgroundColor` | Defines the background color of the sticky header. Will only be used if `useStickyGroupSeparators` is used | no | `Color(0xffF7F7F7)` |
-|`separator` | A Widget which defines a separator between items inside a group | no | no separator |
-| `groupComparator` | Can be used to define a custom sorting for the groups. Otherwise the natural sorting order is used | no | - |
-| `itemComparator` | Can be used to define a custom sorting for the elements inside each group. Otherwise the natural sorting order is used | no | - |
-| `order` | Change to `GroupedListOrder.DESC` to reverse the group sorting | no | `GroupedListOrder.ASC` |
-| `footer` | Widget at the bottom of the list | no | - |
+| Name | Description | Required | Default value |
+|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----|----|
+| `elements` | A list of the data you want to display in the list | required | - |
+| `groupBy` | Function which maps an element to its grouped value | required | - |
+| `itemBuilder` / `indexedItemBuilder` / `interdependentItemBuilder` / `grouItemBuilder` | Function which returns an Widget which defines the item.
* `indexedItemBuilder` provides the current index as well.
* `interdependentItemBuilder` provides the previous and next items as well.
* `groupItemBuilder` provides information if the item is the first or last element inside a group.
`indexedItemBuilder` is preferred over `interdependentItemBuilder` and `interdependentItemBuilder` is preferred over`itemBuilder`. | yes, either of them | - |
+| `groupSeparatorBuilder` / `groupHeaderBuilder` | Function which returns a Widget which defines the group headers. While `groupSeparatorBuilder` gets the `groupBy`-value as parameter `groupHeaderBuilder` gets the whole element. If both are defined `groupHeaderBuilder` is preferred | yes, either of them | - |
+| `groupStickyHeaderBuilder` | Function which returns a Widget which defines the sticky group header, when `useStickyGroupSeparators` is `true`. If not defined `groupSeparatorBuilder` or `groupHeaderBuilder` will be used as described above. | no | - |
+| `useStickyGroupSeparators` | When set to true the group header of the current visible group will stick on top | no | `false` |
+| `floatingHeader` | Whether the sticky group header float over the list or occupy it's own space | no | `false` |
+| `stickyHeaderBackgroundColor` | Defines the background color of the sticky header. Will only be used if `useStickyGroupSeparators` is used | no | `Color(0xffF7F7F7)` |
+| `separator` | A Widget which defines a separator between items inside a group | no | no separator |
+| `groupComparator` | Can be used to define a custom sorting for the groups. Otherwise the natural sorting order is used | no | - |
+| `itemComparator` | Can be used to define a custom sorting for the elements inside each group. Otherwise the natural sorting order is used | no | - |
+| `order` | Change to `GroupedListOrder.DESC` to reverse the group sorting | no | `GroupedListOrder.ASC` |
+| `footer` | Widget at the bottom of the list | no | - |
**Also the fields from `ListView.builder` can be used.**
diff --git a/example/lib/chat_example.dart b/example/lib/chat_example.dart
index f74996b..353d93e 100644
--- a/example/lib/chat_example.dart
+++ b/example/lib/chat_example.dart
@@ -11,100 +11,90 @@ void main() => runApp(const MyApp());
DateTime initialReferenceDate = DateTime(2023, 6, 24, 9, 05);
DateTime _currentReferenceDate = initialReferenceDate;
-DateTime getCurrentReferenceDate(Duration duration){
+DateTime getCurrentReferenceDate(Duration duration) {
_currentReferenceDate = _currentReferenceDate.subtract(duration);
return _currentReferenceDate;
}
String remoteUserName = 'Michael Jackson';
-Color remoteUserColor =
- Colors.blueAccent;
- //Colors.primaries[Random().nextInt(Colors.primaries.length)];
+Color remoteUserColor = Colors.blueAccent;
+//Colors.primaries[Random().nextInt(Colors.primaries.length)];
Duration _scrollDelay = const Duration(seconds: 0);
-List get getOlderMessages =>[
- for (int position = 0; position < 3; position++)
- ...[
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(days: 3, seconds: 40)),
- message: 'Yeah sure I have send them per mail',
- isLocalUser: true
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(seconds: 8)),
- message: 'I dont understand the math questions :(',
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 2)),
- message: 'Can you send me the homework for tomorrow please?'
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 1)),
- message: 'Of course what do you need?',
- isLocalUser: true
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 10)),
- message: 'Hey whats up? Can you help me real quick?'
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(days: 5, minutes: 2)),
- message: 'Okay see you then :)'
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 6)),
- message: 'Lets meet at 8 o clock',
- isLocalUser: true
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 2)),
- message: 'Yes of course when do we want to meet'
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 3)),
- message: 'Hey you do you wanna go to the cinema?',
- isLocalUser: true
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 10)),
- message: 'I am fine too'
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 10)),
- message: 'Fine and what about you?',
- isLocalUser: true
- ),
- Message(
- username: remoteUserName,
- userColor: remoteUserColor,
- date: getCurrentReferenceDate(const Duration(minutes: 10)),
- message: 'Hello how are you?'
- ),
- ]..sort((a,b) => a.compareTo(b))
-];
+List get getOlderMessages => [
+ for (int position = 0; position < 3; position++)
+ ...[
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date:
+ getCurrentReferenceDate(const Duration(days: 3, seconds: 40)),
+ message: 'Yeah sure I have send them per mail',
+ isLocalUser: true),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(seconds: 8)),
+ message: 'I dont understand the math questions :(',
+ ),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 2)),
+ message: 'Can you send me the homework for tomorrow please?'),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 1)),
+ message: 'Of course what do you need?',
+ isLocalUser: true),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 10)),
+ message: 'Hey whats up? Can you help me real quick?'),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date:
+ getCurrentReferenceDate(const Duration(days: 5, minutes: 2)),
+ message: 'Okay see you then :)'),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 6)),
+ message: 'Lets meet at 8 o clock',
+ isLocalUser: true),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 2)),
+ message: 'Yes of course when do we want to meet'),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 3)),
+ message: 'Hey you do you wanna go to the cinema?',
+ isLocalUser: true),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 10)),
+ message: 'I am fine too'),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 10)),
+ message: 'Fine and what about you?',
+ isLocalUser: true),
+ Message(
+ username: remoteUserName,
+ userColor: remoteUserColor,
+ date: getCurrentReferenceDate(const Duration(minutes: 10)),
+ message: 'Hello how are you?'),
+ ]..sort((a, b) => a.compareTo(b))
+ ];
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@@ -114,18 +104,15 @@ class MyApp extends StatefulWidget {
}
class _MyAppState extends State {
-
final List _messages = getOlderMessages;
@override
void initState() {
- SystemChrome.setPreferredOrientations([
- DeviceOrientation.portraitUp
- ]);
+ SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.initState();
}
- void _removeInputTextFocus(){
+ void _removeInputTextFocus() {
FocusScope.of(context).requestFocus(FocusNode());
}
@@ -141,7 +128,6 @@ class _MyAppState extends State {
@override
Widget build(BuildContext context) {
-
ThemeData currentTheme = Theme.of(context);
ThemeData remoteUserTheme = currentTheme;
ThemeData localUserTheme = currentTheme.copyWith(
@@ -151,50 +137,41 @@ class _MyAppState extends State {
),
cardTheme: currentTheme.cardTheme.copyWith(
color: Colors.blue,
- )
- );
+ ));
const BoxDecoration chatBackgroundDecoration = BoxDecoration(
- gradient: LinearGradient(
- colors: [
- Color(0xFFe3edff),
- Color(0xFFcad8fd)
- ]
- )
- );
+ gradient:
+ LinearGradient(colors: [Color(0xFFe3edff), Color(0xFFcad8fd)]));
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Grouped List Chat Example',
theme: ThemeData(
- primarySwatch: Colors.blue,
- canvasColor: Colors.transparent
- ),
+ primarySwatch: Colors.blue, canvasColor: Colors.transparent),
home: Scaffold(
appBar: AppBar(
title: const Text('Grouped List Chat Example'),
),
body: Builder(
- builder: (context) =>
- Container(
- decoration: chatBackgroundDecoration,
- child: Column(
- children: [
- Expanded(
- child: GestureDetector(
- onTap: _removeInputTextFocus,
- child: ChatTimeline(
- messages: _messages,
- localUserTheme: localUserTheme,
- remoteUserTheme: remoteUserTheme,
- onPageTopScrollFunction: _onPageTopScrollFunction,
- ),
+ builder: (context) => Container(
+ decoration: chatBackgroundDecoration,
+ child: Column(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: _removeInputTextFocus,
+ child: ChatTimeline(
+ messages: _messages,
+ localUserTheme: localUserTheme,
+ remoteUserTheme: remoteUserTheme,
+ onPageTopScrollFunction: _onPageTopScrollFunction,
),
),
- const FakeMessageTextField()
- ],
- ),
+ ),
+ const FakeMessageTextField()
+ ],
),
+ ),
),
),
);
@@ -235,11 +212,10 @@ class _ChatTimelineState extends State {
if (widget.onPageTopScrollFunction == null) return;
if (!(_scrollCompleter?.isCompleted ?? true)) return;
- double
- screenSize = MediaQuery.of(context).size.height,
- scrollLimit = _scrollController.position.maxScrollExtent,
- missingScroll = scrollLimit - screenSize,
- scrollLimitActivation = scrollLimit - missingScroll * 0.05;
+ double screenSize = MediaQuery.of(context).size.height,
+ scrollLimit = _scrollController.position.maxScrollExtent,
+ missingScroll = scrollLimit - screenSize,
+ scrollLimitActivation = scrollLimit - missingScroll * 0.05;
if (_scrollController.position.pixels < scrollLimitActivation) return;
if (!(_scrollCompleter?.isCompleted ?? true)) return;
@@ -269,25 +245,23 @@ class _ChatTimelineState extends State {
element.date.month,
element.date.day,
),
- groupHeaderBuilder: (element) =>
- GroupHeaderDate(date: element.date),
+ groupHeaderBuilder: (element) => GroupHeaderDate(date: element.date),
interdependentItemBuilder: (
- context,
- Message? previousElement,
- Message currentElement,
- Message? nextElement,
+ context,
+ Message? previousElement,
+ Message currentElement,
+ Message? nextElement,
) =>
Theme(
- data: currentElement.isLocalUser
- ? widget.localUserTheme
- : widget.remoteUserTheme,
- child: MessageBox(
- context: context,
- previousElement: previousElement,
- currentElement: currentElement,
- nextElement: nextElement
- ),
- ),
+ data: currentElement.isLocalUser
+ ? widget.localUserTheme
+ : widget.remoteUserTheme,
+ child: MessageBox(
+ context: context,
+ previousElement: previousElement,
+ currentElement: currentElement,
+ nextElement: nextElement),
+ ),
),
);
}
@@ -301,55 +275,40 @@ class FakeMessageTextField extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(10.0),
child: ConstrainedBox(
- constraints: BoxConstraints(
- maxWidth: MediaQuery.of(context).size.width * 0.9
- ),
+ constraints:
+ BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.9),
child: Theme(
data: Theme.of(context).copyWith(
- inputDecorationTheme: InputDecorationTheme(
- filled: true,
- fillColor: Colors.white,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16.0,
- vertical: 8.0
- ),
- border: OutlineInputBorder(
- borderSide: BorderSide.none,
- borderRadius: BorderRadius.circular(24.0),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: const BorderRadius.all(Radius.circular(24.0)),
- borderSide: BorderSide(
- color: Theme.of(context).primaryColor,
- width: 2
- )
- ),
- errorBorder: OutlineInputBorder(
- borderRadius: const BorderRadius.all(Radius.circular(24.0)),
- borderSide: BorderSide(
- color: Theme.of(context).colorScheme.error,
- width: 2
- )
- ),
- )
- ),
+ inputDecorationTheme: InputDecorationTheme(
+ filled: true,
+ fillColor: Colors.white,
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ border: OutlineInputBorder(
+ borderSide: BorderSide.none,
+ borderRadius: BorderRadius.circular(24.0),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: const BorderRadius.all(Radius.circular(24.0)),
+ borderSide: BorderSide(
+ color: Theme.of(context).primaryColor, width: 2)),
+ errorBorder: OutlineInputBorder(
+ borderRadius: const BorderRadius.all(Radius.circular(24.0)),
+ borderSide: BorderSide(
+ color: Theme.of(context).colorScheme.error, width: 2)),
+ )),
child: TextField(
minLines: 1,
maxLines: 8,
decoration: InputDecoration(
prefixIcon: IconButton(
onPressed: () => null,
- icon: Icon(
- Icons.camera_alt_outlined,
- color: Theme.of(context).primaryColor
- ),
+ icon: Icon(Icons.camera_alt_outlined,
+ color: Theme.of(context).primaryColor),
),
suffixIcon: IconButton(
onPressed: () => null,
- icon: Icon(
- Icons.send,
- color: Theme.of(context).primaryColor
- ),
+ icon: Icon(Icons.send, color: Theme.of(context).primaryColor),
),
),
),
@@ -359,14 +318,10 @@ class FakeMessageTextField extends StatelessWidget {
}
}
-
class GroupHeaderDate extends StatelessWidget {
final DateTime date;
- const GroupHeaderDate({
- required this.date,
- super.key
- });
+ const GroupHeaderDate({required this.date, super.key});
@override
Widget build(BuildContext context) {
@@ -380,16 +335,15 @@ class GroupHeaderDate extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 12.0,
- vertical: 8.0
- ),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Text(
DateFormat.yMMMd().format(date),
textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Colors.white
- ),
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall
+ ?.copyWith(color: Colors.white),
),
),
),
@@ -398,7 +352,6 @@ class GroupHeaderDate extends StatelessWidget {
}
}
-
class MessageBox extends StatelessWidget {
const MessageBox({
super.key,
@@ -415,26 +368,16 @@ class MessageBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
- bool
- displayUserName = true,
- displayAvatar = true;
+ bool displayUserName = true, displayAvatar = true;
- if (currentElement.isLocalUser){
+ if (currentElement.isLocalUser) {
displayUserName = false;
- } else
-
- if (nextElement == null){
+ } else if (nextElement == null) {
displayUserName = true;
- } else
-
- if (
- DateUtils.dateOnly(currentElement.date) !=
- DateUtils.dateOnly(nextElement!.date)
- ){
+ } else if (DateUtils.dateOnly(currentElement.date) !=
+ DateUtils.dateOnly(nextElement!.date)) {
displayUserName = true;
- } else
-
- if (!nextElement!.isLocalUser){
+ } else if (!nextElement!.isLocalUser) {
displayUserName = false;
}
@@ -453,14 +396,10 @@ class MessageBox extends StatelessWidget {
child: CircleAvatar(
radius: 16,
backgroundColor: currentElement.userColor,
- child: const Icon(
- Icons.person,
- color: Colors.white
- ),
+ child: const Icon(Icons.person, color: Colors.white),
),
),
- if (!displayAvatar)
- const SizedBox(width: 40),
+ if (!displayAvatar) const SizedBox(width: 40),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: currentElement.isLocalUser
@@ -472,9 +411,10 @@ class MessageBox extends StatelessWidget {
padding: const EdgeInsets.only(left: 10.0, top: 10),
child: Text(
'${currentElement.username}:',
- style: Theme.of(context).textTheme.titleSmall?.copyWith(
- color: currentElement.userColor
- ),
+ style: Theme.of(context)
+ .textTheme
+ .titleSmall
+ ?.copyWith(color: currentElement.userColor),
),
),
SizedBox(
@@ -487,32 +427,30 @@ class MessageBox extends StatelessWidget {
elevation: 4.0,
shadowColor: Colors.black45,
shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- bottomLeft: const Radius.circular(18.0),
- topRight: const Radius.circular(18.0),
- topLeft: Radius.circular(
- currentElement.isLocalUser ? 18.0 : 0
- ),
- bottomRight: Radius.circular(
- currentElement.isLocalUser ? 0 : 18.0
- ),
- )
- ),
- margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
+ borderRadius: BorderRadius.only(
+ bottomLeft: const Radius.circular(18.0),
+ topRight: const Radius.circular(18.0),
+ topLeft:
+ Radius.circular(currentElement.isLocalUser ? 18.0 : 0),
+ bottomRight:
+ Radius.circular(currentElement.isLocalUser ? 0 : 18.0),
+ )),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 10.0, vertical: 4.0),
child: Stack(
children: [
Padding(
- padding: const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 24.0),
+ padding:
+ const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 24.0),
child: Text(currentElement.message),
),
Positioned(
- bottom: 4,
- right: 8,
- child: Text(
- DateFormat.Hm().format(currentElement.date),
- style: Theme.of(context).textTheme.bodySmall,
- )
- )
+ bottom: 4,
+ right: 8,
+ child: Text(
+ DateFormat.Hm().format(currentElement.date),
+ style: Theme.of(context).textTheme.bodySmall,
+ ))
],
),
),
@@ -532,13 +470,12 @@ class Message implements Comparable {
String message;
bool isLocalUser;
- Message({
- required this.username,
- required this.userColor,
- required this.date,
- required this.message,
- this.isLocalUser = false
- });
+ Message(
+ {required this.username,
+ required this.userColor,
+ required this.date,
+ required this.message,
+ this.isLocalUser = false});
@override
int compareTo(other) {
diff --git a/example/lib/section_example.dart b/example/lib/section_example.dart
new file mode 100644
index 0000000..06cb158
--- /dev/null
+++ b/example/lib/section_example.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+import 'package:grouped_list/grouped_list.dart';
+
+void main() => runApp(const MyApp());
+
+List _elements = [
+ {'name': 'John', 'group': 'Team A'},
+ {'name': 'Will', 'group': 'Team B'},
+ {'name': 'Beth', 'group': 'Team A'},
+ {'name': 'Miranda', 'group': 'Team B'},
+ {'name': 'Mike', 'group': 'Team C'},
+ {'name': 'Danny', 'group': 'Team C'},
+];
+
+class MyApp extends StatelessWidget {
+ const MyApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ debugShowCheckedModeBanner: false,
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Grouped List View Example'),
+ ),
+ body: _createGroupedListView(),
+ ),
+ );
+ }
+
+ _createGroupedListView() {
+ return GroupedListView(
+ elements: _elements,
+ groupBy: (element) => element['group'],
+ groupComparator: (value1, value2) => value2.compareTo(value1),
+ itemComparator: (item1, item2) => item1['name'].compareTo(item2['name']),
+ order: GroupedListOrder.DESC,
+ useStickyGroupSeparators: true,
+ groupSeparatorBuilder: (String value) => Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ value,
+ textAlign: TextAlign.center,
+ style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ ),
+ itemBuilder: (c, element) {
+ return Card(
+ elevation: 8.0,
+ margin: const EdgeInsets.symmetric(horizontal: 10.0),
+ child: SizedBox(
+ child: ListTile(
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
+ leading: const Icon(Icons.account_circle),
+ title: Text(element['name']),
+ trailing: const Icon(Icons.arrow_forward),
+ ),
+ ),
+ );
+ },
+ groupItemBuilder: (c, element, groupStart, groupEnd) {
+ ShapeBorder? shapeBorder;
+ if (groupStart) {
+ shapeBorder = const RoundedRectangleBorder(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(15), topRight: Radius.circular(15)));
+ } else if (groupEnd) {
+ shapeBorder = const RoundedRectangleBorder(
+ borderRadius: BorderRadius.only(
+ bottomLeft: Radius.circular(15),
+ bottomRight: Radius.circular(15)));
+ }
+ return Card(
+ elevation: 8.0,
+ margin: const EdgeInsets.symmetric(horizontal: 10.0),
+ shape: shapeBorder,
+ child: SizedBox(
+ child: ListTile(
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
+ leading: const Icon(Icons.account_circle),
+ title: Text(element['name']),
+ trailing: const Icon(Icons.arrow_forward),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/grouped_list.dart b/lib/grouped_list.dart
index 7b69671..182947a 100644
--- a/lib/grouped_list.dart
+++ b/lib/grouped_list.dart
@@ -67,6 +67,15 @@ class GroupedListView extends StatefulWidget {
final Widget Function(BuildContext context, T? previousElement,
T currentElement, T? nextElement)? interdependentItemBuilder;
+ /// Called to build children for the list with additional information about
+ /// whether the item is at the start or end of a group.
+ ///
+ /// The [groupStart] parameter is `true` if the item is the first in its group.
+ /// The [groupEnd] parameter is `true` if the item is the last in its group.
+ final Widget Function(
+ BuildContext context, T element, bool groupStart, bool groupEnd)?
+ groupItemBuilder;
+
/// Called to build children for the list with
/// 0 <= element, index < elements.length
final Widget Function(BuildContext context, T element, int index)?
@@ -222,6 +231,7 @@ class GroupedListView extends StatefulWidget {
this.groupStickyHeaderBuilder,
this.emptyPlaceholder,
this.itemBuilder,
+ this.groupItemBuilder,
this.indexedItemBuilder,
this.interdependentItemBuilder,
this.itemComparator,
@@ -251,7 +261,8 @@ class GroupedListView extends StatefulWidget {
this.footer,
}) : assert(itemBuilder != null ||
indexedItemBuilder != null ||
- interdependentItemBuilder != null),
+ interdependentItemBuilder != null ||
+ groupItemBuilder != null),
assert(groupSeparatorBuilder != null || groupHeaderBuilder != null);
@override
@@ -301,6 +312,7 @@ class _GroupedListViewState extends State> {
_sortedElements = _sortElements();
var hiddenIndex = widget.reverse ? _sortedElements.length * 2 - 1 : 0;
var isSeparator = widget.reverse ? (int i) => i.isOdd : (int i) => i.isEven;
+ isValidIndex(int i) => i >= 0 && i < _sortedElements.length;
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
_scrollListener();
@@ -324,16 +336,22 @@ class _GroupedListViewState extends State> {
child: _buildGroupSeparator(_sortedElements[actualIndex]),
);
}
+ var curr = widget.groupBy(_sortedElements[actualIndex]);
+ var preIndex = actualIndex + (widget.reverse ? 1 : -1);
+ var prev = isValidIndex(preIndex)
+ ? widget.groupBy(_sortedElements[preIndex])
+ : null;
+ var nextIndex = actualIndex + (widget.reverse ? -1 : 1);
+ var next = isValidIndex(nextIndex)
+ ? widget.groupBy(_sortedElements[nextIndex])
+ : null;
if (isSeparator(index)) {
- var curr = widget.groupBy(_sortedElements[actualIndex]);
- var prev = widget
- .groupBy(_sortedElements[actualIndex + (widget.reverse ? 1 : -1)]);
if (prev != curr) {
return _buildGroupSeparator(_sortedElements[actualIndex]);
}
return widget.separator;
}
- return _buildItem(context, actualIndex);
+ return _buildItem(context, actualIndex, prev != curr, curr != next);
}
return Stack(
@@ -379,20 +397,35 @@ class _GroupedListViewState extends State> {
/// Returns the widget for element positioned at [index]. The widget is
/// retrieved either by [widget.indexedItemBuilder], [widget.itemBuilder]
/// or [widget.interdependentItemBuilder].
- Widget _buildItem(context, int index) => KeyedSubtree(
+ Widget _buildItem(context, int index, bool groupStart, bool groupEnd) =>
+ KeyedSubtree(
key: _keys.putIfAbsent('$index', () => GlobalKey()),
- child: widget.indexedItemBuilder != null
- ? widget.indexedItemBuilder!(context, _sortedElements[index], index)
- : widget.interdependentItemBuilder != null
- ? widget.interdependentItemBuilder!(
+ child: widget.groupItemBuilder != null
+ ? widget.groupItemBuilder!(
+ context,
+ _sortedElements[index],
+ groupStart,
+ groupEnd,
+ )
+ : widget.indexedItemBuilder != null
+ ? widget.indexedItemBuilder!(
context,
- index > 0 ? _sortedElements[index - 1] : null,
_sortedElements[index],
- index + 1 < _sortedElements.length
- ? _sortedElements[index + 1]
- : null,
+ index,
)
- : widget.itemBuilder!(context, _sortedElements[index]),
+ : widget.interdependentItemBuilder != null
+ ? widget.interdependentItemBuilder!(
+ context,
+ index > 0 ? _sortedElements[index - 1] : null,
+ _sortedElements[index],
+ index + 1 < _sortedElements.length
+ ? _sortedElements[index + 1]
+ : null,
+ )
+ : widget.itemBuilder!(
+ context,
+ _sortedElements[index],
+ ),
);
/// This scroll listener is added to the lists controller if