tree_data_table 1.0.0 copy "tree_data_table: ^1.0.0" to clipboard
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.

Pub Version Build License: MIT

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 #

A simple flat data table

Simple tree table #

A data table with expandable tree rows

Complex tree table #

A complex data table with grouped headers and many columns

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.

0
likes
160
points
7
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter widget for displaying hierarchical data in a table with pagination and pinned columns.

Repository (GitHub)
View/report issues

Topics

#data-table #tree-view #pagination #flutter-widget #lazy-loading

License

MIT (license)

Dependencies

collection, flutter, flutter_localizations, intl

More

Packages that depend on tree_data_table