adding packages
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart' as cupertino;
|
||||
|
||||
class PagedListView extends StatefulWidget {
|
||||
const PagedListView({
|
||||
required this.rows,
|
||||
required this.columns,
|
||||
this.showSelect = true,
|
||||
this.showSort = true,
|
||||
this.mobileItemBuilder,
|
||||
this.selectedActions,
|
||||
this.actions,
|
||||
this.onSelectAll,
|
||||
this.rowsPerPage,
|
||||
this.loadNext,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending,
|
||||
this.onRefresh,
|
||||
this.isRowCountApproximate = false,
|
||||
this.initialScrollOffset = 0,
|
||||
this.noItems,
|
||||
this.isLoading,
|
||||
this.slivers,
|
||||
});
|
||||
|
||||
final double initialScrollOffset;
|
||||
final List<DataColumn> columns;
|
||||
final List<DataRow> rows;
|
||||
final IndexedWidgetBuilder? mobileItemBuilder;
|
||||
final bool showSelect, showSort;
|
||||
final List<Widget>? actions;
|
||||
final List<Widget>? selectedActions;
|
||||
final ValueChanged<bool?>? onSelectAll;
|
||||
final int? rowsPerPage;
|
||||
final VoidCallback? loadNext;
|
||||
final int? sortColumnIndex;
|
||||
final bool? sortAscending;
|
||||
final Widget? noItems, isLoading;
|
||||
final RefreshCallback? onRefresh;
|
||||
final bool isRowCountApproximate;
|
||||
final List<Widget>? slivers;
|
||||
|
||||
@override
|
||||
_NativePagedListViewState createState() => _NativePagedListViewState();
|
||||
}
|
||||
|
||||
class _NativePagedListViewState extends State<PagedListView> {
|
||||
ScrollController? _controller;
|
||||
PersistentBottomSheetController? _sortController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = ScrollController(
|
||||
initialScrollOffset: widget.initialScrollOffset * 40.0,
|
||||
);
|
||||
_controller!.addListener(_scrollListener);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PagedListView oldWidget) {
|
||||
if (oldWidget.showSelect != widget.showSelect) if (mounted) setState(() {});
|
||||
if (oldWidget.showSort != widget.showSort) if (mounted) setState(() {});
|
||||
if (oldWidget.rows != widget.rows) if (mounted) setState(() {});
|
||||
if (oldWidget.columns != widget.columns) if (mounted) setState(() {});
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (_controller!.offset >= _controller!.position.maxScrollExtent && !_controller!.position.outOfRange) {
|
||||
// Bottom of List
|
||||
widget.loadNext!();
|
||||
}
|
||||
if (_controller!.offset <= _controller!.position.minScrollExtent && !_controller!.position.outOfRange) {
|
||||
// Top of List
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: _controller,
|
||||
slivers: <Widget>[
|
||||
...?widget.slivers,
|
||||
if (widget.onRefresh == null) SliverToBoxAdapter(child: Container()) else cupertino.CupertinoSliverRefreshControl(onRefresh: widget.onRefresh),
|
||||
if (widget.isLoading != null && widget.rows.isEmpty)
|
||||
Center(child: widget.isLoading)
|
||||
else
|
||||
widget.noItems != null && widget.rows.isEmpty
|
||||
? Center(child: widget.noItems)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
widget.mobileItemBuilder ??
|
||||
(context, index) {
|
||||
return ExpansionTile(
|
||||
leading: widget.showSelect
|
||||
? Checkbox(
|
||||
value: widget.rows[index].selected,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
widget.rows[index].onSelectChanged!(value);
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
title: widget.rows[index].cells.first.child,
|
||||
children: _buildMobileChildren(index),
|
||||
);
|
||||
},
|
||||
childCount: widget.rows.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.showSelect || widget.showSort)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: cupertino.BorderSide(color: Colors.grey[200]!),
|
||||
)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: rowsSelected ? selectedActions : actions,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> get actions => [
|
||||
if (widget.showSelect)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.select_all),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.onSelectAll!(true);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (widget.showSort)
|
||||
IconButton(
|
||||
tooltip: "Sort Items",
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
onPressed: () {
|
||||
if (_sortController != null) {
|
||||
_sortController!.close();
|
||||
debugPrint("Close...");
|
||||
return;
|
||||
}
|
||||
|
||||
_sortController = Scaffold.of(context).showBottomSheet((context) {
|
||||
final List<DataColumn> _cols = widget.columns.where((c) => c.onSort != null).toList();
|
||||
final bool? _sortAsc = widget.sortAscending;
|
||||
final int? selectedIndex = widget.sortColumnIndex;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark ? Colors.black38 : Colors.grey[200],
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
for (var i = 0; i < _cols.length; i++) ...[
|
||||
ListTile(
|
||||
dense: true,
|
||||
selected: selectedIndex == i,
|
||||
title: _cols[i].label,
|
||||
subtitle: Text(widget.sortAscending! ? 'Ascending' : 'Descending'),
|
||||
leading: Radio<int>(
|
||||
groupValue: selectedIndex,
|
||||
onChanged: (value) {
|
||||
_sortController!.setState!(() {
|
||||
_cols[i].onSort!(i, _sortAsc!);
|
||||
});
|
||||
},
|
||||
value: i,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: Icon(_sortAsc! ? Icons.arrow_upward : Icons.arrow_downward),
|
||||
onPressed: () {
|
||||
_sortController!.setState!(() {
|
||||
_cols[i].onSort!(i, !_sortAsc);
|
||||
});
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
if (selectedIndex == i) {
|
||||
_sortController!.setState!(() {
|
||||
_cols[i].onSort!(i, !_sortAsc);
|
||||
});
|
||||
} else {
|
||||
_sortController!.setState!(() {
|
||||
_cols[i].onSort!(i, _sortAsc);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||
child: Container(
|
||||
child: TextButton(
|
||||
child: Text(
|
||||
"Close",
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
onPressed: () {
|
||||
_sortController!.close();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
_sortController!.closed.whenComplete(() {
|
||||
debugPrint("Done");
|
||||
_sortController = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(
|
||||
child: widget.onRefresh == null
|
||||
? null
|
||||
: IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: widget.onRefresh,
|
||||
),
|
||||
),
|
||||
...widget.actions ?? [],
|
||||
];
|
||||
List<Widget> get selectedActions => [
|
||||
IconButton(
|
||||
icon: Icon(Icons.clear_all),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.onSelectAll!(false);
|
||||
});
|
||||
},
|
||||
),
|
||||
...widget.selectedActions ?? []
|
||||
];
|
||||
|
||||
bool get rowsSelected => _selectedRowCount != 0;
|
||||
|
||||
int get _selectedRowCount => widget.rows.where((d) => d.selected).toSet().toList().length;
|
||||
|
||||
List<Widget> _buildMobileChildren(int index) {
|
||||
List<Widget> _children = [];
|
||||
int i = 0;
|
||||
for (var _cell in widget.rows[index].cells) {
|
||||
_children.add(ListTile(
|
||||
title: widget.columns[i].label,
|
||||
subtitle: _cell.child,
|
||||
));
|
||||
i++;
|
||||
}
|
||||
return _children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class StatelessDataTable extends StatelessWidget {
|
||||
StatelessDataTable({
|
||||
Key? key,
|
||||
this.header,
|
||||
this.actions,
|
||||
required this.columns,
|
||||
required this.rows,
|
||||
this.sortColumnIndex,
|
||||
this.showCheckboxColumn = true,
|
||||
this.sortAscending = true,
|
||||
this.totalItems,
|
||||
this.onSelectAll,
|
||||
this.firstRowIndex = 0,
|
||||
this.onPageChanged,
|
||||
this.shrinkWrap = false,
|
||||
this.selectedActions,
|
||||
this.rowCountApproximate = false,
|
||||
this.rowsPerPage = defaultRowsPerPage,
|
||||
this.handlePrevious,
|
||||
this.handleNext,
|
||||
this.availableRowsPerPage = const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
|
||||
this.onRowsPerPageChanged,
|
||||
this.dragStartBehavior = DragStartBehavior.down,
|
||||
}) : assert(columns.isNotEmpty),
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
|
||||
assert(rowsPerPage > 0),
|
||||
assert(() {
|
||||
if (onRowsPerPageChanged != null) {
|
||||
assert(availableRowsPerPage.contains(rowsPerPage));
|
||||
}
|
||||
return true;
|
||||
}()),
|
||||
super(key: key);
|
||||
|
||||
final VoidCallback? handleNext, handlePrevious;
|
||||
final Widget? header;
|
||||
final bool showCheckboxColumn;
|
||||
final List<Widget>? actions, selectedActions;
|
||||
final List<DataColumn> columns;
|
||||
final List<DataRow> rows;
|
||||
final bool shrinkWrap;
|
||||
final int? sortColumnIndex;
|
||||
final int? totalItems;
|
||||
final bool sortAscending;
|
||||
final ValueSetter<bool?>? onSelectAll;
|
||||
final ValueChanged<int?>? onPageChanged;
|
||||
final int rowsPerPage;
|
||||
static const int defaultRowsPerPage = 10;
|
||||
final List<int> availableRowsPerPage;
|
||||
final ValueChanged<int?>? onRowsPerPageChanged;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final int firstRowIndex;
|
||||
final bool rowCountApproximate;
|
||||
final Map<int, DataRow> _rows = <int, DataRow>{};
|
||||
|
||||
DataRow _getBlankRowFor(int index) => DataRow.byIndex(index: index, cells: columns.map<DataCell>((DataColumn column) => DataCell.empty).toList());
|
||||
|
||||
DataRow _getProgressIndicatorRowFor(int index) {
|
||||
bool haveProgressIndicator = false;
|
||||
final List<DataCell> cells = columns.map<DataCell>((DataColumn column) {
|
||||
if (!column.numeric) {
|
||||
haveProgressIndicator = true;
|
||||
return const DataCell(CircularProgressIndicator());
|
||||
}
|
||||
return DataCell.empty;
|
||||
}).toList();
|
||||
if (!haveProgressIndicator) {
|
||||
haveProgressIndicator = true;
|
||||
cells[0] = const DataCell(CircularProgressIndicator());
|
||||
}
|
||||
return DataRow.byIndex(index: index, cells: cells);
|
||||
}
|
||||
|
||||
List<DataRow> _getRows(int firstRowIndex, int rowsPerPage) {
|
||||
final List<DataRow> result = <DataRow>[];
|
||||
final int nextPageFirstRowIndex = firstRowIndex + rowsPerPage;
|
||||
bool haveProgressIndicator = false;
|
||||
for (int index = firstRowIndex; index < nextPageFirstRowIndex; index += 1) {
|
||||
DataRow? row;
|
||||
if (index < rows.length || rowCountApproximate) {
|
||||
try {
|
||||
row = _rows.putIfAbsent(index, () => rows[index]);
|
||||
} catch (e) {
|
||||
print("Row not found => $e");
|
||||
}
|
||||
if (row == null && !haveProgressIndicator) {
|
||||
row ??= _getProgressIndicatorRowFor(index);
|
||||
haveProgressIndicator = true;
|
||||
}
|
||||
row ??= _getBlankRowFor(index);
|
||||
result.add(row);
|
||||
}
|
||||
}
|
||||
//show no data
|
||||
if (result.isEmpty) {
|
||||
var cells = columns.map<DataCell>((DataColumn column) => DataCell.empty).toList();
|
||||
cells[cells.length ~/ 2] = const DataCell(Text('no data'));
|
||||
result.add(DataRow.byIndex(index: 0, cells: cells));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
final GlobalKey _tableKey = GlobalKey();
|
||||
|
||||
int get _selectedRowCount => rows.where((d) => d.selected).toSet().toList().length;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
|
||||
final List<Widget> headerWidgets = <Widget>[];
|
||||
double startPadding = 24;
|
||||
|
||||
if (_selectedRowCount == 0) {
|
||||
if (header != null) {
|
||||
headerWidgets.add(Expanded(child: header!));
|
||||
if (header is ButtonBar) {
|
||||
startPadding = 12.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
headerWidgets.add(Expanded(
|
||||
child: Text(localizations.selectedRowCountTitle(_selectedRowCount)),
|
||||
));
|
||||
}
|
||||
if (selectedActions != null && _selectedRowCount != 0) {
|
||||
headerWidgets.addAll(selectedActions!.map<Widget>((Widget action) {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 24.0 - 8.0 * 2.0),
|
||||
child: action,
|
||||
);
|
||||
}).toList());
|
||||
} else if (actions != null) {
|
||||
headerWidgets.addAll(actions!.map<Widget>((Widget action) {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 24.0 - 8.0 * 2.0),
|
||||
child: action,
|
||||
);
|
||||
}).toList());
|
||||
}
|
||||
|
||||
final TextStyle? footerTextStyle = themeData.textTheme.caption;
|
||||
final List<Widget> footerWidgets = <Widget>[];
|
||||
if (onRowsPerPageChanged != null) {
|
||||
final List<Widget> _footerChildren =
|
||||
availableRowsPerPage.where((int value) => value <= rows.length || value == rowsPerPage).map<DropdownMenuItem<int>>((int value) {
|
||||
return DropdownMenuItem<int>(value: value, child: Text('$value'));
|
||||
}).toList();
|
||||
footerWidgets.addAll(<Widget>[
|
||||
Container(width: 14),
|
||||
Text(localizations.rowsPerPageTitle),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 64),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
items: _footerChildren as List<DropdownMenuItem<int>>?,
|
||||
value: rowsPerPage,
|
||||
onChanged: onRowsPerPageChanged,
|
||||
style: footerTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
footerWidgets.addAll(<Widget>[
|
||||
Container(width: 32),
|
||||
Text(localizations.pageRowsInfoTitle(firstRowIndex + 1, firstRowIndex + rowsPerPage, totalItems ?? rows.length, rowCountApproximate)),
|
||||
Container(width: 32),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: localizations.previousPageTooltip,
|
||||
onPressed: firstRowIndex <= 0 ? null : handlePrevious),
|
||||
Container(width: 24),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: localizations.nextPageTooltip,
|
||||
onPressed: (!rowCountApproximate && (firstRowIndex + rowsPerPage >= (totalItems ?? rows.length))) ? null : handleNext),
|
||||
Container(width: 14),
|
||||
]);
|
||||
|
||||
if (shrinkWrap) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (showCheckboxColumn)
|
||||
Semantics(
|
||||
container: true,
|
||||
child: DefaultTextStyle(
|
||||
style: _selectedRowCount > 0
|
||||
? themeData.textTheme.subtitle1!.copyWith(color: themeData.colorScheme.secondary)
|
||||
: themeData.textTheme.headline6!.copyWith(fontWeight: FontWeight.w400),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(opacity: 0.54),
|
||||
child: ButtonTheme(
|
||||
child: Ink(
|
||||
height: 64,
|
||||
color: _selectedRowCount > 0 ? themeData.secondaryHeaderColor : null,
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: startPadding, end: 14),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: headerWidgets),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final rows = _getRows(firstRowIndex, rowsPerPage);
|
||||
return DataTable(
|
||||
showCheckboxColumn: showCheckboxColumn,
|
||||
key: _tableKey,
|
||||
columns: columns,
|
||||
sortColumnIndex: sortColumnIndex,
|
||||
sortAscending: sortAscending,
|
||||
onSelectAll: onSelectAll,
|
||||
rows: rows,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: footerTextStyle!,
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(opacity: 0.54),
|
||||
child: Container(
|
||||
height: 56,
|
||||
child: SingleChildScrollView(
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
child: Row(
|
||||
children: footerWidgets,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (showCheckboxColumn)
|
||||
Semantics(
|
||||
container: true,
|
||||
child: DefaultTextStyle(
|
||||
style: _selectedRowCount > 0
|
||||
? themeData.textTheme.subtitle1!.copyWith(color: themeData.colorScheme.secondary)
|
||||
: themeData.textTheme.headline6!.copyWith(fontWeight: FontWeight.w400),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(opacity: 0.54),
|
||||
child: ButtonTheme(
|
||||
child: Ink(
|
||||
height: 64,
|
||||
color: _selectedRowCount > 0 ? themeData.secondaryHeaderColor : null,
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: startPadding, end: 14),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: headerWidgets),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: ScrollConfiguration(
|
||||
behavior: CustomScrollBehavior(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
key: _tableKey,
|
||||
columns: columns,
|
||||
sortColumnIndex: sortColumnIndex,
|
||||
sortAscending: sortAscending,
|
||||
onSelectAll: onSelectAll,
|
||||
rows: _getRows(firstRowIndex, rowsPerPage)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: footerTextStyle!,
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(opacity: 0.54),
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: SingleChildScrollView(
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
child: Row(
|
||||
children: footerWidgets,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user