- Ready for a large number of data. Building cells on demand.
- Focused on Web/Desktop Applications.
- Bidirectional scroll bars.
- Resizable.
- Highly customized.
- Pinned columns.
- Multiple sortable.
- Infinite scroll.
Usage
- Get started
- Column
- Row
- Cell
- Theme
- Support this project
Get started
EasyTableModel<Person>? _model;
@override
void initState() {
super.initState();
_model = EasyTableModel<Person>(rows: [
Person('Landon', 19),
Person('Sari', 22),
Person('Julian', 37),
Person('Carey', 39),
Person('Cadu', 43),
Person('Delmar', 72)
], columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(name: 'Age', intValue: (row) => row.age)
]);
}
@override
Widget build(BuildContext context) {
return EasyTable<Person>(_model);
}
Column
Columns fit
All columns will fit in the available width.
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', grow: 2, stringValue: (row) => row.name),
EasyTableColumn(name: 'Age', grow: 1, intValue: (row) => row.age)
]);
EasyTable<Person>(_model,
columnWidthBehavior: ColumnWidthBehavior.fit);
Stretchable column
The remaining width will be distributed to the columns according to the value of the grow
attribute.
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', grow: 1, stringValue: (row) => row.name),
EasyTableColumn(name: 'Age', intValue: (row) => row.age)
]);
EasyTable<Person>(_model);
Multiple sort
EasyTable(_model, multiSort: true);
Column style
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(
name: 'Age',
intValue: (row) => row.age,
headerTextStyle: TextStyle(color: Colors.blue[900]!),
headerAlignment: Alignment.center,
cellAlignment: Alignment.center,
cellTextStyle: TextStyle(color: Colors.blue[700]!),
cellBackground: (data) => Colors.blue[50])
]);
Pinned column
_model = EasyTableModel(rows: persons, columns: [
EasyTableColumn(
pinStatus: PinStatus.left,
width: 30,
cellBuilder: (BuildContext context, RowData<Person> data) {
return InkWell(
child: const Icon(Icons.edit, size: 16),
onTap: () => _onEdit(data.row));
}),
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(name: 'Age', intValue: (row) => row.age)
]);
Row
Row color
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(name: 'Age', intValue: (row) => row.age)
]);
@override
Widget build(BuildContext context) {
return EasyTable<Person>(_model, rowColor: _rowColor);
}
Color? _rowColor(RowData<Person> data) {
if (data.row.age < 20) {
return Colors.green[50]!;
} else if (data.row.age > 30 && data.row.age < 50) {
return Colors.orange[50]!;
}
return null;
}
Row cursor
EasyTableTheme(
child: EasyTable<Person>(_model,
rowCursor: (data) =>
data.row.age < 20 ? SystemMouseCursors.forbidden : null),
data: const EasyTableThemeData(
row: RowThemeData(cursorOnTapGesturesOnly: false)));
Row callbacks
@override
Widget build(BuildContext context) {
return EasyTable<Person>(_model,
onRowTap: (person) => _onRowTap(context, person),
onRowSecondaryTap: (person) => _onRowSecondaryTap(context, person),
onRowDoubleTap: (person) => _onRowDoubleTap(context, person));
}
void _onRowTap(BuildContext context, Person person) {
...
}
void _onRowSecondaryTap(BuildContext context, Person person) {
...
}
void _onRowDoubleTap(BuildContext context, Person person) {
...
}
Row hover listener
EasyTable<Person>(_model, onHover: _onHover);
void _onHover(int? rowIndex) {
...
}
Infinite scroll
EasyTableModel<Value>? _model;
bool _loading = false;
@override
void initState() {
super.initState();
List<Value> rows = List.generate(30, (index) => Value(index));
_model = EasyTableModel<Value>(rows: rows, columns: [
EasyTableColumn(name: 'Index', intValue: (row) => row.index),
EasyTableColumn(name: 'Random 1', stringValue: (row) => row.random1),
EasyTableColumn(name: 'Random 2', stringValue: (row) => row.random2)
]);
}
@override
Widget build(BuildContext context) {
return EasyTable<Value>(_model,
lastRowWidget: const LoadingWidget(),
onLastRowWidget: _onLastRowWidget);
}
void _onLastRowWidget(bool visible) {
if (visible && !_loading) {
setState(() {
_loading = true;
});
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_loading = false;
List<Value> newValues =
List.generate(15, (index) => Value(_model!.rowsLength + index));
_model!.addRows(newValues);
});
});
}
}
Cell
Cell style
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(
name: 'Age',
intValue: (row) => row.age,
cellStyleBuilder: (data) => data.row.age >= 30 && data.row.age < 40
? CellStyle(
background: Colors.blue[800],
alignment: Alignment.center,
textStyle: const TextStyle(color: Colors.white))
: null)
]);
Custom cell widget
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(
name: 'Rate',
width: 150,
cellBuilder: (context, data) => StarsWidget(stars: data.row.stars))
]);
Cell edit
class Person {
Person(this.name, this.value);
final String name;
final int value;
bool _valid = true;
bool get valid => _valid;
String _editable = '';
String get editable => _editable;
set editable(String value) {
_editable = value;
_valid = _editable.length < 6;
}
}
class MainWidgetState extends State<MainWidget> {
EasyTableModel<Person>? _model;
@override
void initState() {
super.initState();
List<Person> rows = [
Person('Landon', 1),
Person('Sari', 0),
Person('Julian', 2),
Person('Carey', 4),
Person('Cadu', 5),
Person('Delmar', 2)
];
_model = EasyTableModel<Person>(rows: rows, columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(name: 'Value', intValue: (row) => row.value),
EasyTableColumn(
name: 'Editable',
cellBuilder: _buildField,
cellBackground: (rowData) =>
rowData.row.valid ? null : Colors.red[800])
]);
}
Widget _buildField(BuildContext context, RowData<Person> rowData) {
return TextFormField(
initialValue: rowData.row.editable,
style:
TextStyle(color: rowData.row.valid ? Colors.black : Colors.white),
onChanged: (value) => _onFieldChange(value, rowData.row));
}
void _onFieldChange(String value, Person person) {
final wasValid = person.valid;
person.editable = value;
if (wasValid != person.valid) {
setState(() {
// rebuild
});
}
}
@override
Widget build(BuildContext context) {
return EasyTable<Person>(_model);
}
}
Theme
Dividers thickness and color
EasyTableTheme(
child: EasyTable<Person>(_model),
data: const EasyTableThemeData(
columnDividerThickness: 4,
columnDividerColor: Colors.blue,
header: HeaderThemeData(columnDividerColor: Colors.purple),
row: RowThemeData(dividerThickness: 4, dividerColor: Colors.green),
scrollbar:
TableScrollbarThemeData(columnDividerColor: Colors.orange)));
Header
EasyTableTheme(
child: EasyTable<Person>(_model),
data: EasyTableThemeData(
header: HeaderThemeData(
color: Colors.green[50],
bottomBorderHeight: 4,
bottomBorderColor: Colors.blue),
headerCell: HeaderCellThemeData(
height: 40,
alignment: Alignment.center,
textStyle: const TextStyle(
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
color: Colors.blue),
resizeAreaWidth: 10,
resizeAreaHoverColor: Colors.blue.withOpacity(.5),
sortIconColor: Colors.green,
expandableName: false)));
Hidden header
EasyTableTheme(
child: EasyTable<Person>(_model),
data:
const EasyTableThemeData(header: HeaderThemeData(visible: false)));
Row
Theme Row color
EasyTableTheme(
data: EasyTableThemeData(
row: RowThemeData(color: (rowIndex) => Colors.green[50])),
child: EasyTable<Person>(_model));
Row zebra color
EasyTableTheme(
data: EasyTableThemeData(
row: RowThemeData(color: RowThemeData.zebraColor())),
child: EasyTable<Person>(_model));
Row hover background
EasyTableTheme(
data: EasyTableThemeData(
row: RowThemeData(hoverBackground: (rowIndex) => Colors.blue[50])),
child: EasyTable<Person>(_model));
Row hover foreground
EasyTableTheme(
data: EasyTableThemeData(
row: RowThemeData(
hoverForeground: (rowIndex) => Colors.blue.withOpacity(.2))),
child: EasyTable<Person>(_model));
Row fill height
EasyTableTheme(
data: EasyTableThemeData(
row: RowThemeData(
fillHeight: true, color: RowThemeData.zebraColor())),
child: EasyTable<Person>(_model));
Scrollbar
EasyTableTheme(
child: EasyTable<Person>(_model),
data: const EasyTableThemeData(
scrollbar: TableScrollbarThemeData(
thickness: 16,
thumbColor: Colors.black,
pinnedHorizontalColor: Colors.yellow,
unpinnedHorizontalColor: Colors.green,
verticalColor: Colors.blue,
borderThickness: 8,
pinnedHorizontalBorderColor: Colors.orange,
unpinnedHorizontalBorderColor: Colors.purple,
verticalBorderColor: Colors.pink)));
Scrollbar always visible
EasyTableTheme(
data: const EasyTableThemeData(
scrollbar: TableScrollbarThemeData(
horizontalOnlyWhenNeeded: false,
verticalOnlyWhenNeeded: false)),
child: EasyTable<Person>(_model));
A warning is being displayed in the console due to a bug in Flutter: https://github.com/flutter/flutter/issues/103939. The error happens when the horizontal scrollbar is hidden after being visible. The PR (https://github.com/flutter/flutter/pull/103948) fix it.
Cell
Null value color
_model = EasyTableModel<Person>(rows: [
Person('Landon', '+321 321-432-543'),
Person('Sari', '+123 456-789-012'),
Person('Julian', null),
Person('Carey', '+111 222-333-444'),
Person('Cadu', null),
Person('Delmar', '+22 222-222-222')
], columns: [
EasyTableColumn(name: 'Name', stringValue: (row) => row.name),
EasyTableColumn(
name: 'Mobile', width: 150, stringValue: (row) => row.mobile)
]);
EasyTableTheme(
child: EasyTable<Person>(_model),
data: EasyTableThemeData(
cell: CellThemeData(
nullValueColor: ((rowIndex, hovered) => Colors.grey[300]))));
TODO
- Collapsed rows
- Header grouping
- Row selection
- Column reorder
- Cell merge
- Pinned column on right
- Filter
- And everything else, the sky is the limit
Support this project
Bitcoin
bc1qhqy84y45gya58gtfkvrvass38k4mcyqnav803h
Ethereum (ERC-20) or Binance Smart Chain (BEP-20)
0x9eB815FD4c88A53322304143A9Aa8733D3369985
Solana
7vp45LoQXtLYFXXKx8wQGnzYmhcnKo1TmfqUgMX45Ad8