Skip to content
Blog

DataTable in Flutter: Sorting, Selection, and Styling Tabular Data

Use Flutter's DataTable for sortable, selectable tabular data — plus when to switch to DataTableSource and how to style and scroll it correctly.

Published on • November 2, 2026

AI Assistant

Tabular data is a first-class Material pattern, and Flutter’s DataTable gives you sorting, selection, and styling with a small API (docs.flutter.dev). The catch: it builds every row eagerly, so knowing when to graduate to DataTableSource matters.

DataTable vs DataTableSource

DataTable renders all its rows in memory — fine for a few dozen rows. For large or remote sets, implement DataTableSource and feed it to PaginatedDataTable, which pages rows lazily and drives sorting through the same DataColumn.onSort callback. Either way, wrap the table in a horizontal SingleChildScrollView: DataTable has no built-in horizontal scrolling and will overflow narrow screens.

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: DataTable(
    columns: const [
      DataColumn(label: Text('Name')),
      DataColumn(label: Text('Role'), numeric: true),
    ],
    rows: const [
      DataRow(cells: [DataCell(Text('Ada')), DataCell(Text('Admin'))]),
      DataRow(cells: [DataCell(Text('Linus')), DataCell(Text('Dev'))]),
    ],
  ),
)

Sorting with DataColumn.onSort

Give each sortable column an onSort that flips sortAscending and records sortColumnIndex, then sort a copy of your data before building rows — the header arrow follows that state automatically.

int _sortColumn = 0;
bool _sortAscending = true;

DataColumn _sortable(String label, int col) => DataColumn(
      label: Text(label),
      onSort: (c, asc) => setState(() {
        _sortColumn = c;
        _sortAscending = asc;
      }),
    );

final sorted = [...students]..sort((a, b) {
      final cmp = _sortColumn == 0
          ? a.name.compareTo(b.name)
          : a.age.compareTo(b.age);
      return _sortAscending ? cmp : -cmp;
    });

Selection with DataRow.onSelectChanged

Track selection in a Set<int> (or a set of ids) and wire each row’s onSelectChanged; set showCheckboxColumn: false if selection is row-tap-only.

final Set<int> _selected = {};

DataRow _row(int i, Student s) => DataRow(
      selected: _selected.contains(i),
      onSelectChanged: (v) => setState(
          () => v == true ? _selected.add(i) : _selected.remove(i)),
      cells: [DataCell(Text(s.name)), DataCell(Text('${s.age}'))],
    );

Styling, states, and when to go custom

  • Set headingRowColor: WidgetStatePropertyAll(...) per table, or theme every table at once with DataTableTheme.
  • Render loading as a centered CircularProgressIndicator and empty as a DataTable with rows: const [] plus an explanatory row — never leave a blank grid.
  • Build a custom table (a SliverList of Rows) when you need frozen columns, cell spanning, or per-cell widgets beyond DataCell.

DataTable covers sorting, selection, and styling for small sets — the moment your rows outgrow memory, hand the same callbacks to a paginated DataTableSource.