better_data_table 0.2.2 copy "better_data_table: ^0.2.2" to clipboard
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 widthsFlexColumnWidth, FixedColumnWidth, IntrinsicColumnWidth, FractionColumnWidth, or mix minWidth/maxWidth constraints
  • Sortable columns — built-in ascending/descending arrow icons, onSort callback
  • Row selection — leading checkbox column, select-all tristate, custom checkbox builder
  • Hierarchical / nested rows — add children to any BetterDataTableRow for 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 rowsBetterDataTableTheme.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 interactionsonRowTap, onRowDoubleTap, onRowLongPress, onRowSecondaryTap, onCellTap
  • Per-row dividersrowDivider: BorderSide(...) paints a full-width line between rows
  • Column tooltipsBetterDataTableColumn(tooltip: 'hint')
  • Row tooltipsBetterDataTableRow(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 BetterDataTableTheme and its copyWith
  • copyWith everywhereBetterDataTableColumn, BetterDataTableRow, BetterDataTableGroup, and BetterDataTableTheme all 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
)
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.
0
likes
160
points
176
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A highly customizable Flutter table widget with sorting, selection, expandable rows, collapsible groups, column visibility, striped rows, and a comprehensive theming system.

Repository (GitHub)
View/report issues

Topics

#table #data-table #grid #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on better_data_table