tree_data_table 0.0.1-beta.1
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;
}