tree_data_table 0.0.1-beta.1 copy "tree_data_table: ^0.0.1-beta.1" to clipboard
tree_data_table: ^0.0.1-beta.1 copied to clipboard

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

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tree_data_table/tree_data_table.dart';

import 'source/platform/platform_action_loader.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const TreeDataTableExample());
}

class TreeDataTableExample extends StatefulWidget {
  const TreeDataTableExample({super.key});

  @override
  State<TreeDataTableExample> createState() => _TreeDataTableExampleState();
}

class _TreeDataTableExampleState extends State<TreeDataTableExample> {
  late ValueNotifier<ThemeMode> _themeMode;

  @override
  void initState() {
    super.initState();
    _themeMode = ValueNotifier(ThemeMode.light);
  }

  @override
  void dispose() {
    _themeMode.dispose();
    super.dispose();
  }

  void _toggleTheme() {
    _themeMode.value = _themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: _themeMode,
      builder: (context, themeMode, _) {
        return MaterialApp(
          title: 'Tree Data Table',
          localizationsDelegates: TreeDataTableLocalizations.localizationsDelegates,
          supportedLocales: TreeDataTableLocalizations.supportedLocales,
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(
              seedColor: Colors.deepPurple,
              brightness: Brightness.light,
            ),
            brightness: Brightness.light,
          ),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(
              seedColor: Colors.deepPurple,
              brightness: Brightness.dark,
            ),
            brightness: Brightness.dark,
          ),
          themeMode: themeMode,
          home: ExampleTabsPage(
            onToggleTheme: _toggleTheme,
            themeMode: themeMode,
          ),
        );
      },
    );
  }
}

class ExampleTabsPage extends StatefulWidget {
  final VoidCallback onToggleTheme;
  final ThemeMode themeMode;

  const ExampleTabsPage({
    super.key,
    required this.onToggleTheme,
    required this.themeMode,
  });

  @override
  State<ExampleTabsPage> createState() => _ExampleTabsPageState();
}

class _ExampleTabsPageState extends State<ExampleTabsPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();

    final platformAction = createPlatformAction();

    int initialIndex = platformAction.getInitialIndex();

    _tabController = TabController(
      length: 4,
      vsync: this,
      initialIndex: initialIndex,
    );
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Tree Data Table Examples'),
        actions: [
          IconButton(
            icon: Icon(
              widget.themeMode == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode,
            ),
            tooltip: widget.themeMode == ThemeMode.dark ? 'Switch to Light Mode' : 'Switch to Dark Mode',
            onPressed: widget.onToggleTheme,
          ),
        ],
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: 'Table'),
            Tab(text: 'Simple Tree Table'),
            Tab(text: 'Complex Tree Table'),
            Tab(text: 'Lazy Complex Tree Table'),
          ],
        ),
      ),
      body: SafeArea(
        child: TabBarView(
          controller: _tabController,
          physics: const NeverScrollableScrollPhysics(),
          children: [
            SimpleTableExample(),
            SimpleTreeTableExample(),
            ComplexTreeTableExample(),
            LazyComplexTreeTableExample(),
          ],
        ),
      ),
    );
  }
}

// --- Example 1: Simple Table ---
class SimpleTableExample extends StatefulWidget {
  const SimpleTableExample({
    super.key,
  });

  @override
  State<SimpleTableExample> createState() => _SimpleTableExample();
}

class _SimpleTableExample extends State<SimpleTableExample> with AutomaticKeepAliveClientMixin<SimpleTableExample> {
  @override
  Widget build(BuildContext context) {
    super.build(context);

    final headerRows = [
      HeaderRow(
        columns: [
          HeaderColumn(
            widget: const Text('Name', style: TextStyle(fontWeight: FontWeight.bold)),
            textVisibilityDialog: 'Name',
            width: 300,
            minimumWidth: 200,
            sortable: true,
          ),
          HeaderColumn(
            widget: const Text('Age', style: TextStyle(fontWeight: FontWeight.bold)),
            textVisibilityDialog: 'Age',
            flex: 2.0,
            sortable: true,
          ),
          HeaderColumn(
            widget: const Text('Country', style: TextStyle(fontWeight: FontWeight.bold)),
            textVisibilityDialog: 'Country',
            flex: 0.7,
          ),
        ],
      ),
    ];

    final treeRowNodes = [
      TreeRowNode(
        columns: [
          TreeColumn(widget: const Text('Alice'), sortValue: 'Alice'),
          TreeColumn(widget: const Text('30'), sortValue: 30),
          TreeColumn(widget: const Text('USA')),
        ],
      ),
      TreeRowNode(
        columns: [
          TreeColumn(widget: const Text('Bob'), sortValue: 'Bob'),
          TreeColumn(widget: const Text('25'), sortValue: 25),
          TreeColumn(widget: const Text('Canada')),
        ],
      ),
      TreeRowNode(
        columns: [
          TreeColumn(widget: const Text('Charlie'), sortValue: 'Charlie'),
          TreeColumn(widget: const Text('35'), sortValue: 35),
          TreeColumn(widget: const Text('UK')),
        ],
      ),
    ];

    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 1010),
        child: SizedBox(
          height: 500,
          width: double.infinity,
          child: TreeDataTable.eager(
            headerRows: headerRows,
            dataSource: TreeDataTableEagerDataSource(rows: treeRowNodes),
            persistColumnStateId: 'simple_table_example',
            scaleColumnsToFit: true,
          ),
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

enum ColumnAutoSizeExampleScenario {
  inferred,
  disabled,
  fixedWidth,
  builderWidth,
}

class ColumnAutoSizeExample extends StatelessWidget {
  final ColumnAutoSizeExampleScenario scenario;

  const ColumnAutoSizeExample({
    super.key,
    this.scenario = ColumnAutoSizeExampleScenario.inferred,
  });

  @override
  Widget build(BuildContext context) {
    final autoSize = scenario == ColumnAutoSizeExampleScenario.disabled ? null : const ColumnAutoSize();
    final autoSizeWidth = switch (scenario) {
      ColumnAutoSizeExampleScenario.fixedWidth => 120.0,
      ColumnAutoSizeExampleScenario.builderWidth => 80.0,
      _ => null,
    };
    final ColumnAutoSizeWidthBuilder? autoSizeWidthBuilder =
        scenario == ColumnAutoSizeExampleScenario.builderWidth ? (_) => 240 : null;
    final usesCustomHeader =
        scenario == ColumnAutoSizeExampleScenario.fixedWidth || scenario == ColumnAutoSizeExampleScenario.builderWidth;

    return Center(
      child: SizedBox(
        width: 700,
        height: 400,
        child: TreeDataTable.eager(
          headerRows: [
            HeaderRow(
              columns: [
                HeaderColumn(
                  widget: usesCustomHeader
                      ? const SizedBox(width: 400, child: Text('Custom header'))
                      : const Text('Description'),
                  textVisibilityDialog: 'Description',
                  width: 500,
                  minimumWidth: 50,
                  autoSize: autoSize,
                  autoSizeWidth: autoSizeWidth,
                  autoSizeWidthBuilder: autoSizeWidthBuilder,
                ),
                HeaderColumn(
                  widget: const Text('Count'),
                  textVisibilityDialog: 'Count',
                  width: 160,
                ),
              ],
            ),
          ],
          dataSource: TreeDataTableEagerDataSource(
            rows: [
              TreeRowNode(
                columns: [
                  TreeColumn(
                    widget: scenario == ColumnAutoSizeExampleScenario.inferred
                        ? const Text('A longer visible description with some extra example content')
                        : const Text('7'),
                  ),
                  TreeColumn(widget: const Text('7')),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// --- Example 2: Simple Tree Table ---
class SimpleTreeTableExample extends StatefulWidget {
  const SimpleTreeTableExample({
    super.key,
  });

  @override
  State<SimpleTreeTableExample> createState() => _SimpleTreeTableExample();
}

class _SimpleTreeTableExample extends State<SimpleTreeTableExample>
    with AutomaticKeepAliveClientMixin<SimpleTreeTableExample> {
  @override
  Widget build(BuildContext context) {
    super.build(context);

    final headerRows = [
      HeaderRow(
        columns: [
          HeaderColumn(
            widget: const Text('Category', style: TextStyle(fontWeight: FontWeight.bold)),
            textVisibilityDialog: 'Category',
            flex: 1.0,
            minimumWidth: 150,
          ),
          HeaderColumn(
            widget: const Text('Value', style: TextStyle(fontWeight: FontWeight.bold)),
            textVisibilityDialog: 'Value',
            flex: 1.0,
          ),
        ],
      ),
    ];

    final treeRowNodes = [
      TreeRowNode(
        columns: [
          TreeColumn(widget: const Text('Fruits')),
          TreeColumn(widget: const Text('')),
        ],
        rows: [
          TreeRowNode(
            columns: [
              TreeColumn(widget: const Text('Apple')),
              TreeColumn(widget: const Text('10')),
            ],
          ),
          TreeRowNode(
            columns: [
              TreeColumn(widget: const Text('Banana')),
              TreeColumn(widget: const Text('20')),
            ],
          ),
        ],
      ),
      TreeRowNode(
        columns: [
          TreeColumn(widget: const Text('Vegetables')),
          TreeColumn(widget: const Text('')),
        ],
        rows: [
          TreeRowNode(
            columns: [
              TreeColumn(widget: const Text('Carrot')),
              TreeColumn(widget: const Text('15')),
            ],
          ),
          TreeRowNode(
            columns: [
              TreeColumn(widget: const Text('Broccoli')),
              TreeColumn(widget: const Text('12')),
            ],
          ),
        ],
      ),
    ];

    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 1010),
        child: SizedBox(
          height: 500,
          width: double.infinity,
          child: TreeDataTable.eager(
            headerRows: headerRows,
            dataSource: TreeDataTableEagerDataSource(
              rows: treeRowNodes,
              rowsPerPage: [5, 10],
            ),
            persistColumnStateId: 'simple_tree_table_example',
            scaleColumnsToFit: true,
          ),
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

// --- Example 3: Complex Tree Table ---
class ComplexTreeTableExample extends StatefulWidget {
  final int lengthTableData;
  final double heightTable;

  const ComplexTreeTableExample({
    super.key,
    this.lengthTableData = 3000,
    this.heightTable = 500,
  });

  @override
  State<ComplexTreeTableExample> createState() => _ComplexTreeTableExampleState();
}

class _TableData {
  final List<HeaderRow> headerRows;
  final List<TreeRowNode> treeRowNodes;

  _TableData(this.headerRows, this.treeRowNodes);
}

class _ComplexTreeTableExampleState extends State<ComplexTreeTableExample>
    with AutomaticKeepAliveClientMixin<ComplexTreeTableExample> {
  late ValueNotifier<Future<_TableData>?> _tableDataFuture;

  @override
  void initState() {
    super.initState();
    _tableDataFuture = ValueNotifier(null);

    WidgetsBinding.instance.addPostFrameCallback((_) async {
      final future = compute(_generateTableData, widget.lengthTableData);
      _tableDataFuture.value = future;
    });
  }

  @override
  void dispose() {
    _tableDataFuture.dispose();
    super.dispose();
  }

  static Future<_TableData> _generateTableData(int lengthTableData) async {
    int length = lengthTableData;
    int columnsCount = 10;
    int maxDepth = 4;

    List<HeaderRow> headerRows = [
      HeaderRow(
        columns: List.generate(
          columnsCount - 2,
          (index) => HeaderColumn(
            width: index == 0
                ? 250
                : index == 1
                    ? 230
                    : index == 7
                        ? null
                        : 200,
            flex: index == 7 ? 1.5 : null,
            minimumWidth: index == 7 ? 170 : null,
            maximumWidth: index == 4 ? 250 : null,
            combinedColumns: index == 1 ? 3 : null,
            resizable: index == 5 ? false : true,
            visible: index == 6 ? false : true,
            widget: Text(
              'Header 1 Column $index',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
            textVisibilityDialog: 'Header 1 Column $index',
            pinLeft: index == 0,
          ),
        ),
      ),
      HeaderRow(
        columns: List.generate(
          columnsCount,
          (index) => HeaderColumn(
            width: index == 0
                ? 250
                : index == 1
                    ? 230
                    : index == 9
                        ? null
                        : 200,
            flex: index == 9 ? 1.5 : null,
            minimumWidth: index == 0
                ? 160
                : index == 9
                    ? 170
                    : null,
            resizable: index == 7 ? false : true,
            visible: index == 8 ? false : true,
            canHide: index == 6 ? false : true,
            widget: Text(
              'Header 2 Column $index',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
            textVisibilityDialog: 'Header 2 Column $index',
            pinLeft: index == 0,
          ),
        ),
      ),
    ];

    List<TreeRowNode> treeRowNodes = List.generate(
      length,
      (index) => _buildTreeNode(
        level: 1,
        maxDepth: maxDepth,
        columnsCount: columnsCount,
        labelPrefix: 'Row $index',
        variationSeed: index,
      ),
    );

    return _TableData(headerRows, treeRowNodes);
  }

  static TreeRowNode _buildTreeNode({
    required int level,
    required int maxDepth,
    required int columnsCount,
    required String labelPrefix,
    required int variationSeed,
  }) {
    return TreeRowNode(
      columns: List.generate(
        columnsCount,
        (index) {
          final text = '$labelPrefix Column $index';
          final hasLongText = (variationSeed + level + index * 3) % 11 == 0;

          return TreeColumn(
            widget: Text(
              hasLongText ? '$text with some extra example content' : text,
            ),
          );
        },
      ),
      rows: level < maxDepth
          ? List.generate(
              2,
              (childIdx) => _buildTreeNode(
                level: level + 1,
                maxDepth: maxDepth,
                columnsCount: columnsCount,
                labelPrefix: 'Level ${level + 1} (${childIdx + 1})',
                variationSeed: variationSeed * 2 + childIdx + 1,
              ),
            )
          : [],
    );
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return Center(
      child: SizedBox(
        height: widget.heightTable.toDouble(),
        width: 1010,
        child: ValueListenableBuilder<Future<_TableData>?>(
          valueListenable: _tableDataFuture,
          builder: (context, tableDataFuture, _) {
            return FutureBuilder<_TableData?>(
              future: tableDataFuture,
              builder: (context, snapshot) {
                if (snapshot.connectionState != ConnectionState.done) {
                  return const Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircularProgressIndicator(),
                        SizedBox(height: 16),
                        Text('Loading table data...'),
                      ],
                    ),
                  );
                }

                if (snapshot.hasError) {
                  return Center(child: Text('Error: ${snapshot.error}'));
                }

                final data = snapshot.data!;

                return Column(
                  children: <Widget>[
                    Expanded(
                      child: TreeDataTable.eager(
                        headerPinned: true,
                        headerMinExtent: 102,
                        headerMaxExtent: 102,
                        headerRows: data.headerRows,
                        dataSource: TreeDataTableEagerDataSource(
                          rows: data.treeRowNodes,
                          rowsPerPage: const [5, 10, 25, 100, 500],
                        ),
                        persistColumnStateId: 'complex_tree_table_example',
                      ),
                    ),
                  ],
                );
              },
            );
          },
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

// --- Example 4: Lazy Complex Tree Table ---
class LazyComplexTreeTableExample extends StatefulWidget {
  final int lengthTableData;
  final double heightTable;

  const LazyComplexTreeTableExample({
    super.key,
    this.lengthTableData = 3000000,
    this.heightTable = 500,
  });

  @override
  State<LazyComplexTreeTableExample> createState() => _LazyComplexTreeTableExampleState();
}

class _LazyComplexTreeNodeId {
  final int level;
  final int variationSeed;

  const _LazyComplexTreeNodeId({
    required this.level,
    required this.variationSeed,
  });
}

class _LazyComplexTreeTableExampleState extends State<LazyComplexTreeTableExample>
    with AutomaticKeepAliveClientMixin<LazyComplexTreeTableExample> {
  static const int _columnsCount = 10;
  static const int _maxDepth = 4;

  static List<HeaderRow> _buildHeaderRows() {
    return [
      HeaderRow(
        columns: List.generate(
          _columnsCount - 2,
          (index) => HeaderColumn(
            width: index == 0
                ? 250
                : index == 1
                    ? 230
                    : index == 7
                        ? null
                        : 200,
            flex: index == 7 ? 1.5 : null,
            minimumWidth: index == 7 ? 170 : null,
            maximumWidth: index == 4 ? 250 : null,
            combinedColumns: index == 1 ? 3 : null,
            resizable: index == 5 ? false : true,
            visible: index == 6 ? false : true,
            widget: Text(
              'Header 1 Column $index',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
            textVisibilityDialog: 'Header 1 Column $index',
            pinLeft: index == 0,
          ),
        ),
      ),
      HeaderRow(
        columns: List.generate(
          _columnsCount,
          (index) => HeaderColumn(
            width: index == 0
                ? 250
                : index == 1
                    ? 230
                    : index == 9
                        ? null
                        : 200,
            flex: index == 9 ? 1.5 : null,
            minimumWidth: index == 0
                ? 160
                : index == 9
                    ? 170
                    : null,
            resizable: index == 7 ? false : true,
            visible: index == 8 ? false : true,
            canHide: index == 6 ? false : true,
            widget: Text(
              'Header 2 Column $index',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
            textVisibilityDialog: 'Header 2 Column $index',
            pinLeft: index == 0,
          ),
        ),
      ),
    ];
  }

  Future<TreeDataTablePage> _loadPage(int pageIndex, int pageSize) async {
    final firstRowIndex = pageIndex * pageSize;
    final rowCount = (widget.lengthTableData - firstRowIndex).clamp(0, pageSize);

    return TreeDataTablePage(
      rows: List.generate(rowCount, (index) {
        final rootIndex = firstRowIndex + index;
        return _buildTreeNode(
          level: 1,
          labelPrefix: 'Row $rootIndex',
          variationSeed: rootIndex,
        );
      }),
      totalRowCount: widget.lengthTableData,
    );
  }

  Future<List<TreeRowNode>> _loadChildren(TreeRowNode parent) async {
    final id = parent.id;
    if (id is! _LazyComplexTreeNodeId || id.level >= _maxDepth) {
      return const [];
    }

    return List.generate(
      2,
      (childIndex) => _buildTreeNode(
        level: id.level + 1,
        labelPrefix: 'Level ${id.level + 1} (${childIndex + 1})',
        variationSeed: id.variationSeed * 2 + childIndex + 1,
      ),
    );
  }

  static TreeRowNode _buildTreeNode({
    required int level,
    required String labelPrefix,
    required int variationSeed,
  }) {
    return TreeRowNode(
      id: _LazyComplexTreeNodeId(
        level: level,
        variationSeed: variationSeed,
      ),
      columns: List.generate(
        _columnsCount,
        (index) {
          final text = '$labelPrefix Column $index';
          final hasLongText = (variationSeed + level + index * 3) % 11 == 0;

          return TreeColumn(
            widget: Text(
              hasLongText ? '$text with some extra example content' : text,
            ),
          );
        },
      ),
      hasChildren: level < _maxDepth,
    );
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return Center(
      child: SizedBox(
        height: widget.heightTable.toDouble(),
        width: 1010,
        child: TreeDataTable.lazy(
          headerPinned: true,
          headerMinExtent: 102,
          headerMaxExtent: 102,
          headerRows: _buildHeaderRows(),
          dataSource: TreeDataTableLazyDataSource(
            pageLoader: _loadPage,
            childrenLoader: _loadChildren,
            rowsPerPage: const [5, 10, 25, 100, 500],
            maxLevel: _maxDepth - 1,
          ),
          persistColumnStateId: 'lazy_complex_tree_table_example',
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}
0
likes
0
points
7
downloads

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

License

unknown (license)

Dependencies

collection, flutter, flutter_localizations, intl

More

Packages that depend on tree_data_table