Skip to content

Instantly share code, notes, and snippets.

@thetrav
Created February 24, 2020 07:47
Show Gist options
  • Save thetrav/790045859ad8e4e5df56a26387dcd550 to your computer and use it in GitHub Desktop.
Save thetrav/790045859ad8e4e5df56a26387dcd550 to your computer and use it in GitHub Desktop.
flutter list with filter and sort
import 'package:flutter/material.dart';
import 'gd_button.dart';
import 'gd_flex_row.dart';
import 'gt_text_field.dart';
import 'gd_green_bar.dart';
typedef FilterFunction<T> = bool Function(String, T);
typedef SortFunction<T> = int Function(T, T);
typedef HeaderBuilder<T> = Widget Function(BuildContext, FilterFunction<T>, SortFunction<T>);
typedef Listener<T> = void Function(List<T>);
bool Function(String, T) makeFilter<T>(
String Function(T) extract
) => (String s, T t) => extract(t)?.contains(s) ?? false;
int Function(T, T) makeSort<T>(
Comparable Function(T) extract
) => (T a, T b) {
var ea = extract(a);
var eb = extract(b);
return ea == eb ? 0 : ea == null ? -1 : eb == null ? 1 : ea.compareTo(eb);
};
class GdListViewHeader<T> {
final String label;
final FilterFunction<T> filter;
final SortFunction<T> sort;
final Widget Function(BuildContext) builder;
GdListViewHeader(this.label, {this.filter, this.sort, this.builder});
}
class GdListView<T> extends StatefulWidget {
final List<T> list;
final List<GdListViewHeader<T>> headers;
final Widget Function(BuildContext, T) rowBuilder;
final Listener<T> listener;
GdListView({@required this.list, @required this.headers, @required this.rowBuilder, this.listener});
@override
State<StatefulWidget> createState() => _GdListViewState<T>();
}
typedef WrappedFilterFunction<T> = bool Function(T);
class _GdListViewState<T> extends State<GdListView<T>> {
List<T> data;
SortFunction<T> sort;
String sortColumn;
Map<String, String> filterValues;
@override
void initState() {
super.initState();
var header = widget.headers.first;
sort = header.sort;
sortColumn = header.label;
filterValues = Map.fromIterable(
widget.headers,
key: (e) => e.label,
value: (e) => ""
);
applyFilterAndSort(initial: true);
}
@override
didUpdateWidget(GdListView<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if(oldWidget.headers != widget.headers) {
var header = widget.headers.first;
sort = header.sort;
sortColumn = header.label;
filterValues = Map.fromIterable(
widget.headers,
key: (e) => e.label,
value: (e) => ""
);
}
if(oldWidget.list != widget.list) {
applyFilterAndSort(initial: true);
}
}
//NOTE: only call from within setState or initState
void applyFilterAndSort({bool initial: false}) {
data = widget.list.where((row) =>
widget.headers.every((header) {
var filter = header.filter;
var value = filterValues[header.label];
return filter != null ? filter(value, row) : true;
})
).toList();
if(sort != null) {
data.sort((a, b) {
return sort(a, b);
});
}
if(!initial && widget.listener != null) {
widget.listener(data);
}
}
@override
Widget build(BuildContext context) {
var children = <Widget>[
GdGreenBar(
GdFlexRow([
...widget.headers.map((header) => buildHeader(context, header))
])
),
Expanded(child: ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int i) => widget.rowBuilder(context, data[i]),
))
];
return Column(children: children);
}
Widget buildHeader(BuildContext context, GdListViewHeader<T> header) {
final label = header.label;
return _GdListHeader(
builder: header.builder ?? (_) => GdGreenBarText(label),
applyFilter: header.filter == null ? null : (String value) => setState(() {
filterValues[label] = value;
applyFilterAndSort();
}),
applySort: header.sort == null ? null : (){
setState(() {
sortColumn = label;
if(this.sort == header.sort) {
var oldSort = this.sort;
this.sort = (T a, T b) => oldSort(b, a);
} else {
this.sort = header.sort;
}
applyFilterAndSort();
});
},
ascending: sortColumn == label ? sort == this.sort : null
);
}
}
class _GdListHeader extends StatefulWidget {
final void Function(String) applyFilter;
final void Function() applySort;
final bool ascending;
final Widget Function(BuildContext) builder;
_GdListHeader({this.builder, this.applyFilter, this.applySort, this.ascending});
@override
State<StatefulWidget> createState() => _GdListHeaderState();
}
class _GdListHeaderState extends State<_GdListHeader> {
bool showFilter;
TextEditingController filter;
@override
void initState() {
super.initState();
showFilter = false;
filter = TextEditingController();
filter.addListener(() => widget.applyFilter(filter.text));
}
@override
Widget build(BuildContext context) {
var children = <Widget>[];
if(widget.applySort == null) {
children.add(widget.builder(context));
} else {
children.add(InkWell(
child: Row(children: [
widget.builder(context),
Icon(
widget.ascending ?? true ?
Icons.arrow_drop_down :
Icons.arrow_drop_up,
color: widget.ascending == null ?
Colors.grey.shade700 :
Colors.white70,
)
]),
onTap: widget.applySort
));
}
if(widget.applyFilter != null) {
if (showFilter) {
children.add(Row(children: [
Flexible(child: GdTextField("Filter", filter, false)),
GdButton.from(Icon(Icons.close), () =>
setState(() {
showFilter = false;
filter.text = "";
}))
]));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children
);
}
children.add(GdButton.from(Icon(Icons.search, color: Colors.white, size: 16),
() => setState(() => showFilter = true))
);
}
return Row(children: children);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment