better_data_table 0.2.2
better_data_table: ^0.2.2 copied to clipboard
A highly customizable Flutter table widget with sorting, selection, expandable rows, collapsible groups, column visibility, striped rows, and a comprehensive theming system.
better_data_table #
A highly customizable Flutter table widget built on top of Flutter's native
Table. It covers the common table patterns you reach for in data-heavy apps —
without locking you into a rigid design.
Features #
- Flexible column widths —
FlexColumnWidth,FixedColumnWidth,IntrinsicColumnWidth,FractionColumnWidth, or mixminWidth/maxWidthconstraints - Sortable columns — built-in ascending/descending arrow icons,
onSortcallback - Row selection — leading checkbox column, select-all tristate, custom checkbox builder
- Hierarchical / nested rows — add
childrento anyBetterDataTableRowfor infinitely deep sub-rows, indented automatically per level (like ClickUp tasks) - Expandable rows — tap an indicator to reveal arbitrary content below a row
- Collapsible groups — named sections that users can collapse to hide rows
- Column visibility — hide/show columns at runtime with
visible: false - Striped / alternating rows —
BetterDataTableTheme.striped(context) - Footer rows — for totals, averages, or any summary data
- Loading & empty states — sensible defaults with custom builder overrides
- Hover effects — mouse-region highlighting (disable for touch-only targets)
- Row & cell interactions —
onRowTap,onRowDoubleTap,onRowLongPress,onRowSecondaryTap,onCellTap - Per-row dividers —
rowDivider: BorderSide(...)paints a full-width line between rows - Column tooltips —
BetterDataTableColumn(tooltip: 'hint') - Row tooltips —
BetterDataTableRow(tooltip: 'hint') - Pointer cursor on clickable rows — rows with tap handlers automatically show a pointer cursor on desktop/web
- Fully customizable icons — replace sort arrows, row expand/collapse toggles,
and group chevrons with any widget via
BetterDataTableTheme - Comprehensive theming — override every decoration, padding, alignment, and
text style via
BetterDataTableThemeand itscopyWith copyWitheverywhere —BetterDataTableColumn,BetterDataTableRow,BetterDataTableGroup, andBetterDataTableThemeall support immutable updates
Getting started #
Add the dependency to your pubspec.yaml:
dependencies:
better_data_table: ^0.1.0
Then import it:
import 'package:better_data_table/better_data_table.dart';
Usage #
Basic table #
BetterDataTable(
columns: [
BetterDataTableColumn(header: Text('Name'), width: FlexColumnWidth(2)),
BetterDataTableColumn(header: Text('Age'), width: FixedColumnWidth(80)),
BetterDataTableColumn(header: Text('City'), width: FlexColumnWidth(1)),
],
rows: [
BetterDataTableRow(cells: [Text('Alice'), Text('28'), Text('New York')]),
BetterDataTableRow(cells: [Text('Bob'), Text('34'), Text('Berlin')]),
],
)
Sortable columns #
BetterDataTable(
sortColumnIndex: _sortIndex,
sortAscending: _ascending,
onSort: (index) {
setState(() {
if (_sortIndex == index) {
_ascending = !_ascending;
} else {
_sortIndex = index;
_ascending = true;
}
// sort your data list here
});
},
columns: [
BetterDataTableColumn(header: Text('Name'), sortable: true),
BetterDataTableColumn(header: Text('Age'), sortable: true),
],
rows: _people.map((p) =>
BetterDataTableRow(cells: [Text(p.name), Text('${p.age}')])).toList(),
)
Row selection with checkboxes #
BetterDataTable(
showCheckboxes: true,
selectedRows: _selected,
onRowSelected: (index) => setState(() {
_selected.contains(index)
? _selected.remove(index)
: _selected.add(index);
}),
onSelectAll: (value) => setState(() {
_selected = value == true
? Set.from(List.generate(_rows.length, (i) => i))
: {};
}),
columns: [ /* ... */ ],
rows: _rows,
)
Hierarchical / nested rows #
Add children to any BetterDataTableRow to create an infinitely deep tree of
sub-rows. The expand/collapse toggle appears automatically, and each level is
indented by 24 px. Expand state is tracked by a dot-separated path string
("0", "0.1", "0.1.2", …).
// State: Set<String> _expanded = {};
BetterDataTable(
expandedRows: _expanded,
onRowExpanded: (path) => setState(() {
_expanded.contains(path)
? _expanded.remove(path)
: _expanded.add(path);
}),
columns: [
BetterDataTableColumn(header: Text('Task'), width: FlexColumnWidth(3)),
BetterDataTableColumn(header: Text('Status'), width: FlexColumnWidth(1)),
BetterDataTableColumn(header: Text('Priority'), width: FixedColumnWidth(100)),
],
rows: [
BetterDataTableRow(
cells: [Text('Project Alpha'), Text('In Progress'), Text('High')],
children: [
BetterDataTableRow(
cells: [Text('Phase 1'), Text('Completed'), Text('High')],
children: [
BetterDataTableRow(
cells: [Text('Research'), Text('Done'), Text('Medium')],
),
BetterDataTableRow(
cells: [Text('Define requirements'), Text('Done'), Text('High')],
// nest deeper — no limit
children: [
BetterDataTableRow(
cells: [Text('Write specs'), Text('Done'), Text('High')],
),
],
),
],
),
BetterDataTableRow(
cells: [Text('Phase 2'), Text('In Progress'), Text('High')],
),
],
),
BetterDataTableRow(
cells: [Text('Standalone task'), Text('Completed'), Text('Low')],
),
],
)
Expandable rows (custom content) #
Use expandableRowBuilder when you want to show arbitrary content below a row
rather than sub-rows. The builder returns null to suppress the toggle on
specific rows.
// State: Set<String> _expanded = {};
BetterDataTable(
expandedRows: _expanded,
onRowExpanded: (path) => setState(() {
_expanded.contains(path)
? _expanded.remove(path)
: _expanded.add(path);
}),
expandableRowBuilder: (context, row, index) => Padding(
padding: const EdgeInsets.all(16),
child: Text('Details for row $index'),
),
columns: [ /* ... */ ],
rows: _rows,
)
Expandable rows (full-width content) #
expandableRowBuilder's content is placed inside the expand column's table
cell, so it's constrained to that column's width. Use
expandedRowChildBuilder instead when the expanded content needs to span the
full width of the table — it renders as its own block between rows rather
than inside a table cell. Returning null for a row falls back to
expandableRowBuilder (if provided) for that row.
Note that table borders/dividers won't draw through this content, since it
sits outside the Table widget that draws them.
BetterDataTable(
expandedRows: _expanded,
onRowExpanded: (path) => setState(() {
_expanded.contains(path)
? _expanded.remove(path)
: _expanded.add(path);
}),
expandedRowChildBuilder: (context, row, index) => Padding(
padding: const EdgeInsets.all(16),
child: Chart(data: row.chartData),
),
columns: [ /* ... */ ],
rows: _rows,
)
Collapsible groups #
BetterDataTable(
collapsedGroups: _collapsed,
onGroupToggled: (index) => setState(() {
_collapsed.contains(index)
? _collapsed.remove(index)
: _collapsed.add(index);
}),
groups: [
BetterDataTableGroup(
header: Text('Fruits'),
startIndex: 0,
endIndex: 2,
collapsible: true,
),
BetterDataTableGroup(
header: Text('Vegetables'),
startIndex: 3,
endIndex: 5,
collapsible: true,
),
],
columns: [ /* ... */ ],
rows: _rows,
)
Striped theme #
BetterDataTable(
theme: BetterDataTableTheme.striped(context),
columns: [ /* ... */ ],
rows: _rows,
)
Hide a column at runtime #
BetterDataTableColumn(
header: Text('Internal ID'),
visible: _showId, // toggle this to show/hide the column
)
Footer row #
BetterDataTable(
footerRows: [
BetterDataTableRow(cells: [Text('Total'), Text('\$99.00')]),
],
columns: [ /* ... */ ],
rows: _rows,
)
Custom theme #
BetterDataTable(
theme: BetterDataTableTheme.defaultTheme(context).copyWith(
cellPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
headerTextStyle: TextStyle(fontWeight: FontWeight.w900),
rowDivider: BorderSide(color: Colors.grey.shade200),
),
columns: [ /* ... */ ],
rows: _rows,
)
Custom icons #
Every icon used by the table can be replaced with any widget via
BetterDataTableTheme. Pass null (the default) to keep the built-in icon.
| Theme field | Default icon | Where it appears |
|---|---|---|
sortAscendingIcon |
Icons.arrow_upward (16 px) |
Sorted column header — ascending |
sortDescendingIcon |
Icons.arrow_downward (16 px) |
Sorted column header — descending |
rowExpandIcon |
Icons.expand_more (20 px) |
Collapsed expandable / hierarchical row |
rowCollapseIcon |
Icons.expand_less (20 px) |
Expanded expandable / hierarchical row |
groupExpandIcon |
Icons.chevron_right (20 px) |
Collapsed group header |
groupCollapseIcon |
Icons.expand_more (20 px) |
Expanded group header |
BetterDataTable(
theme: BetterDataTableTheme.defaultTheme(context).copyWith(
// Replace sort arrows with triangles
sortAscendingIcon: Icon(Icons.arrow_drop_up, size: 20, color: Colors.blue),
sortDescendingIcon: Icon(Icons.arrow_drop_down, size: 20, color: Colors.blue),
// Use custom SVG or any widget for row expand toggles
rowExpandIcon: Icon(Icons.add_circle_outline, size: 18),
rowCollapseIcon: Icon(Icons.remove_circle_outline, size: 18),
// Custom group chevrons
groupExpandIcon: Icon(Icons.chevron_right, size: 20),
groupCollapseIcon: Icon(Icons.expand_more, size: 20),
),
columns: [ /* ... */ ],
rows: _rows,
)
Pointer cursor on clickable rows #
Rows that have any tap handler (onRowTap, onRowDoubleTap, onRowLongPress,
onRowSecondaryTap, or BetterDataTableRow.onTap) automatically show the
system pointer cursor on desktop and web — no extra configuration needed.
BetterDataTable(
onRowTap: (index) => print('tapped row $index'),
columns: [ /* ... */ ],
rows: _rows,
)
Additional information #
- The table has no built-in scrolling. Wrap it in a
SingleChildScrollView(horizontal and/or vertical) when content may overflow. - File issues and feature requests on GitHub.
- Contributions are welcome — please open a PR with tests for any new behaviour.