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 withDataTableTheme. - Render loading as a centered
CircularProgressIndicatorand empty as aDataTablewithrows: const []plus an explanatory row — never leave a blank grid. - Build a custom table (a
SliverListofRows) when you need frozen columns, cell spanning, or per-cell widgets beyondDataCell.
DataTable covers sorting, selection, and styling for small sets — the moment your rows outgrow memory, hand the same callbacks to a paginated DataTableSource.