tree_data_table 0.0.1-beta.2
tree_data_table: ^0.0.1-beta.2 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.
Beta: The API is still evolving and may introduce breaking changes before the first stable release. Test the package carefully before using it in production.
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
- Columns pinned to either edge, including interactive pinning from the header menu
- Column visibility controls and session-scoped column state persistence
- Optional root-row pagination
- Lazy/server-side root pagination and on-demand child loading
- Single-level and grouped, multi-level headers
- 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(
widget: const Text('Name'),
textVisibilityDialog: 'Name',
flex: 2,
minimumWidth: 160,
pinLeft: true,
),
HeaderColumn(
widget: const Text('Role'),
textVisibilityDialog: 'Role',
flex: 1,
),
],
),
],
dataSource: TreeDataTableEagerDataSource(
rows: [
TreeRowNode(
isExpanded: true,
columns: [
TreeColumn(widget: const Text('Engineering')),
TreeColumn(widget: const Text('Team')),
],
rows: [
TreeRowNode(
columns: [
TreeColumn(widget: const Text('Ada')),
TreeColumn(widget: const Text('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.
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. |
Data-source options control the rows and pagination:
| Data source | Options |
|---|---|
TreeDataTableEagerDataSource |
rows, optional rowsPerPage, rowsPerPageIndex, and maxLevel |
TreeDataTableLazyDataSource |
pageLoader, rowsPerPage, and optional childrenLoader, loadingBuilder, 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(
widget: const Text('Description'),
textVisibilityDialog: '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(
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(
widget: const Text('Status'),
textVisibilityDialog: '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(
widget: const Text('Name'),
textVisibilityDialog: 'Name',
width: 200,
sortable: true,
)
TreeColumn(
widget: Text(customer.name),
sortValue: 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.
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,
);
},
childrenLoader: (parent) async {
final children = await api.fetchChildren(parent.id);
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. maxLevel is required when a 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 and 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.
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.
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.