tree_data_table 1.0.0
tree_data_table: ^1.0.0 copied to clipboard
A Flutter widget for displaying hierarchical data in a table with pagination and pinned columns.
Tree Data Table #
A flexible Flutter data table for flat and hierarchical data, with expandable rows, pagination, resizable columns, and pinned columns.
Features #
- Flat tables and expandable tree rows of arbitrary depth
- Fixed-width and flex columns with minimum and maximum widths
- Drag-to-resize and double-click best-fit column sizing
- Column pinning, visibility controls, reordering, and session-scoped column state persistence
- Optional sorting and built-in text, number, date, date-time, and set filters
- Single and multiple row selection with row and cell interaction callbacks
- Optional root-row pagination
- Lazy/server-side root pagination and on-demand child loading
- Single-level and grouped, multi-level headers
- Built-in localizations for 16 locales
- Light and dark theme support
Installation #
Add the package to your Flutter project:
flutter pub add tree_data_table
Then import it:
import 'package:tree_data_table/tree_data_table.dart';
Localization #
The built-in controls support 16 locales:
- Arabic (
ar), Czech (cs), Dutch (nl), English (en) - French (
fr), German (de), Italian (it), Japanese (ja) - Korean (
ko), Polish (pl), Portuguese (pt), Russian (ru) - Simplified Chinese (
zh), Spanish (es), Turkish (tr), Ukrainian (uk)
Register the package delegates and supported locales on your app to enable the translations. Flutter will select the matching locale from the device or app configuration:
MaterialApp(
localizationsDelegates: TreeDataTableLocalizations.localizationsDelegates,
supportedLocales: TreeDataTableLocalizations.supportedLocales,
home: const MyHomePage(),
)
Quick start #
TreeDataTable must receive bounded height because it expands to fill the available vertical space. A SizedBox or an Expanded widget is usually the simplest choice.
SizedBox(
height: 420,
child: TreeDataTable.eager(
headerRows: [
HeaderRow(
columns: [
HeaderColumn.text(
'Name',
flex: 2,
minimumWidth: 160,
pinLeft: true,
),
HeaderColumn.text(
'Role',
flex: 1,
),
],
),
],
dataSource: TreeDataTableEagerDataSource(
rows: [
TreeRowNode.fromValues(
isExpanded: true,
values: const ['Engineering', 'Team'],
rows: [
TreeRowNode.fromValues(
values: const ['Ada', 'Developer'],
),
],
),
],
rowsPerPage: const [10, 25, 50],
rowsPerPageIndex: 0,
),
persistColumnStateId: 'people-table',
),
)
Every HeaderColumn requires either width or flex. Each data row should contain the same number of TreeColumn values as the leaf header row.
Use the convenience constructors for common text tables:
HeaderColumn.text('Name', width: 200)creates a text header and reuses the same label for column-visibility controls.TreeColumn.text('Ada')creates a text cell and uses the text as the default sort and filter value.TreeColumn.value(30)renders a raw value as text while keeping the original value for sorting and filtering. Add a formatter for dates, money, and other display-only text.TreeColumn.custom(widget: StatusChip(...))creates a custom widget cell with optional sort, filter, sizing, and interaction metadata.TreeRowNode.fromValues(values: ['Ada', 30])renders values as text while keeping the original values for sorting and filtering.
Use the default HeaderColumn and TreeRowNode constructors for full control
over header and row configuration.
Configuration #
Choose TreeDataTable.eager for rows already available in memory and
TreeDataTable.lazy for rows loaded page by page. Both constructors share these
table options:
| Option | Purpose |
|---|---|
headerRows |
Defines the columns and optional grouped header levels. |
dataSource |
A TreeDataTableEagerDataSource for .eager or a TreeDataTableLazyDataSource for .lazy. |
headerPinned |
Keeps the header visible while rows scroll. Defaults to true. |
headerMinExtent / headerMaxExtent |
Sets the header area's minimum and maximum height. |
persistColumnStateId |
Restores widths, visibility, and pinning for this table during the current app session. |
scaleColumnsToFit |
Recalculates untouched flex columns when the table width changes. |
rowSelectionMode |
Enables single or multiple row selection. Defaults to none. |
selectedRowIds / initialSelectedRowIds |
Controls selection externally or seeds uncontrolled selection. |
onRowSelectionChanged |
Receives the selected row ID set after user selection changes. |
showSelectionCheckboxes |
Adds a leading checkbox column for selectable rows. |
selectRowsOnTap |
Also allows selecting rows by tapping row content. Defaults to false; keep it disabled for checkbox-only selection. |
onFilterChanged |
Receives the active column and quick-filter state after filter changes. |
Data-source options control the rows and pagination:
| Data source | Options |
|---|---|
TreeDataTableEagerDataSource |
rows, optional rowsPerPage, rowsPerPageIndex, and maxLevel |
TreeDataTableLazyDataSource |
pageLoader, rowsPerPage, and optional queryPageLoader, childrenLoader, queryChildrenLoader, childCacheKey, loadingBuilder, cancellableLoadingBuilder, errorBuilder, rowsPerPageIndex, and maxLevel |
Column sizing #
Give a HeaderColumn a fixed width or a flex factor. You can constrain either approach with minimumWidth and maximumWidth:
When both are supplied, width takes precedence.
HeaderColumn.text(
'Description',
flex: 2,
minimumWidth: 180,
maximumWidth: 480,
)
Users can drag a resizable column separator to adjust its width. Set resizable: false to disable manual resizing for a column.
Set scaleColumnsToFit: true on TreeDataTable to let flex columns follow the
available table width. Once a user drags or best-fits a column, that column
keeps its selected width while the remaining untouched flex columns continue
to use the available space.
Best-fit column sizing #
Double-click a resizable column separator to fit the column to its header and visible rows. Common widgets such as Text, icons, padding, and simple rows are measured without mounting duplicate widgets.
For a custom cell widget, provide its preferred content width:
TreeColumn.custom(
widget: CustomerStatusWidget(customer),
autoSizeWidth: 148,
)
You can also compute the value from the current BuildContext with autoSizeWidthBuilder. Configure best-fit behavior on the corresponding header:
HeaderColumn.text(
'Status',
width: 120,
autoSize: const ColumnAutoSize(
range: ColumnAutoSizeRange.loadedRows,
sampleLimit: 500,
additionalPadding: 8,
),
)
By default, best fit considers the header and up to 200 visible rows. Use ColumnAutoSizeRange.loadedRows to sample rows loaded for the current page, set includeHeader: false to ignore the header, or set autoSize: null to disable best fit. The result includes cell padding and tree indentation, then respects minimumWidth and maximumWidth.
Pagination #
Pagination applies to root rows; child rows stay with their parent. The two data source classes make the loading contract explicit.
For a normal table, supply all rows up front. Pagination is optional:
TreeDataTable.eager(
headerRows: headerRows,
dataSource: TreeDataTableEagerDataSource(
rows: rows,
rowsPerPage: const [25, 50, 100],
),
)
Omit rowsPerPage to display every eager root row on one page. Page sizes must
be unique positive integers.
Sorting #
Sorting is opt-in for each leaf column. Set sortable: true on its
HeaderColumn and provide a sortValue for every matching TreeColumn:
HeaderColumn.text(
'Name',
width: 200,
sortable: true,
)
TreeColumn.text(customer.name)
Pressing the header cycles through ascending, descending, and unsorted. The
column menu hides the currently active direction and shows unsort only while
that column is sorted. Sorting keeps equal values in their original order and
sorts children within their own sibling group. Supply sortComparator on
HeaderColumn for non-Comparable values or custom ordering. Eager tables
sort the complete data set before pagination; lazy tables sort the currently
loaded page.
Filtering #
Filtering is opt-in for each leaf column. Set filterable: true on its
HeaderColumn and provide filterValue on matching TreeColumn values when
the displayed widget is not enough:
HeaderColumn.text(
'Name',
width: 200,
filterable: true,
)
TreeColumn.text(customer.name)
The column menu includes built-in text, number, date, date-time, and set filters, plus clear-column filter and clear-all filters actions. Text filters support contains, does not contain, equals, does not equal, starts with, ends with, blank, and not blank. Number, date, and date-time filters support equals, does not equal, comparison operators, between, blank, and not blank. Set filters show a searchable checklist of distinct values from the currently loaded rows.
Eager tables filter the complete root set before pagination. Tree filtering keeps ancestors of matching descendants, while descendants under a matching parent still need to match their own filters before they are shown.
For server-side sorting and filtering, supply queryPageLoader on
TreeDataTableLazyDataSource. It receives a TreeDataTablePageQuery with page,
sort, and filter state so a backend can apply the query without local row
mutation. When queryPageLoader is supplied, lazy sort and filter changes reload
page zero from the server instead of sorting or filtering only the currently
loaded page.
Use onFilterChanged on TreeDataTable when the surrounding application needs
to observe the active filter state.
Lazy loading #
For large or remote datasets, use TreeDataTable.lazy. Its page loader receives
the zero-based page index and selected page size, and returns only that page plus
the total number of root rows:
TreeDataTable.lazy(
headerRows: headerRows,
dataSource: TreeDataTableLazyDataSource(
rowsPerPage: const [25, 50, 100],
maxLevel: 3,
pageLoader: (pageIndex, pageSize) async {
final result = await api.fetchRows(
offset: pageIndex * pageSize,
limit: pageSize,
);
return TreeDataTablePage(
rows: result.rows.map(buildTreeRowNode).toList(),
totalRowCount: result.totalCount,
);
},
queryPageLoader: (query) async {
final result = await api.fetchRows(
offset: query.pageIndex * query.pageSize,
limit: query.pageSize,
sortColumnIndex: query.sortColumnIndex,
sortDirection: query.sortDirection,
filters: query.filterState,
);
return TreeDataTablePage(
rows: result.rows.map(buildTreeRowNode).toList(),
totalRowCount: result.totalCount,
);
},
queryChildrenLoader: (query) async {
final children = await api.fetchChildren(
parentId: query.parentId,
sortColumnIndex: query.sortColumnIndex,
sortDirection: query.sortDirection,
filters: query.filterState,
expandedRowIds: query.expandedRowIds,
);
return children.map(buildTreeRowNode).toList();
},
),
)
Set id and hasChildren: true on a TreeRowNode whose rows have not been loaded.
The child loader runs on its first expansion and the returned children are
cached on that node. Use queryChildrenLoader together with queryPageLoader
when child requests need the active parent ID, sort state, filter state, or
expanded row IDs. Use childrenLoader only for simpler parent-only child
requests. maxLevel is required when either child loader is used because the
table cannot infer depth from descendants that have not been requested.
Use loadingBuilder to replace the default progress indicator, or
cancellableLoadingBuilder when the loading UI should expose a cancel action.
Use errorBuilder to present a custom root-page error. The error builder
receives the error and a retry callback. A failed child request remains
unloaded and is retried when the row is collapsed and expanded again.
Advanced lazy loading
Change childCacheKey on TreeDataTableLazyDataSource to discard cached lazy
children after an external data refresh. Query-aware child caches are also
invalidated when server-side sort or filter state changes.
To migrate from the lower-level loaders, keep pageLoader as a fallback and add
queryPageLoader for root rows. Replace childrenLoader with
queryChildrenLoader when the backend needs active query state; only one child
loader can be supplied at a time.
The example app's Lazy Complex Tree Table uses this lazy source end to end and can report hundreds of thousands of rows without constructing them up front.
Expansion state #
TreeRowNode.isExpanded supplies the initial expansion state and is updated by
the table after user interaction. Use TreeRowNode.onExpansionChanged when the
surrounding application also needs to react to that state change.
Non-null row IDs must be unique within the loaded table. A TreeRowNode must
also occur in only one place: shared node instances and cycles are rejected.
Row selection #
Enable row selection with rowSelectionMode. Selection is keyed by
TreeRowNode.id, so every loaded row must have a stable, unique non-null ID when
selection is enabled. Selection is independent for parent and child rows.
TreeDataTable.eager(
rowSelectionMode: TreeDataTableRowSelectionMode.multiple,
initialSelectedRowIds: const {'customer-1'},
showSelectionCheckboxes: true,
onRowSelectionChanged: (selectedIds) {
debugPrint('Selected rows: $selectedIds');
},
headerRows: headerRows,
dataSource: TreeDataTableEagerDataSource(rows: rows),
)
Use selectedRowIds for controlled selection. Use initialSelectedRowIds to
seed the table's internal selection state. TreeRowNode.selectable disables
selection for an individual row, and TreeRowNode.onSelectedChanged is called
when an uncontrolled user selection changes that row. By default, visible
checkboxes are the only row-selection target; set selectRowsOnTap: true only
when row-content taps should also select rows.
Row and cell callbacks #
Use row and cell callbacks to react to interactions without wrapping cell
widgets manually. Configure row callbacks on TreeRowNode and cell callbacks on
TreeColumn. Row callbacks receive the row node, row ID, sibling row index,
visible row index, and row path. Cell callbacks include the same row context plus
display/source column indexes, the header definition, and the original cell
definition.
TreeDataTable.eager(
headerRows: headerRows,
dataSource: TreeDataTableEagerDataSource(
rows: [
TreeRowNode(
id: 'customer-1',
onTap: (event) {
debugPrint('Opened row ${event.rowId} at ${event.visibleRowIndex}');
},
columns: [
TreeColumn.text(
'Ada',
onTap: (event) {
debugPrint(
'Tapped ${event.headerColumn.textVisibilityDialog}: '
'${event.filterValue ?? event.sortValue ?? event.sourceColumnIndex}',
);
},
),
],
),
],
),
)
Available row callbacks are onTap, onDoubleTap, and onSecondaryTap.
Available cell callbacks are onTap, onDoubleTap, onSecondaryTap, and
onHover. Expansion controls handle their own taps and do not emit row or cell
callbacks.
Column state #
Set a unique persistColumnStateId for each table whose interactive state should survive widget rebuilds. Width, visibility, and pinning are kept in memory for the current app session; they are not written to permanent device storage.
Examples #
The example app contains complete flat, simple tree, and multi-level tree tables.
Simple table #
Simple tree table #
Complex tree table #
Run the example locally:
cd example
flutter run
Issues and contributions #
Found a bug or have an idea? Search the issue tracker before opening a new issue. Pull requests are welcome in the GitHub repository.
License #
Tree Data Table is available under the MIT License.
