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.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tree_data_table/tree_data_table.dart';
import 'source/lazy_project_server/lazy_project_server.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 _IconText extends StatelessWidget {
final IconData icon;
final String text;
final Color? color;
const _IconText({
required this.icon,
required this.text,
this.color,
});
@override
Widget build(BuildContext context) {
final effectiveColor = color ?? Theme.of(context).colorScheme.primary;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: effectiveColor),
const SizedBox(width: 8),
Flexible(child: Text(text, overflow: TextOverflow.ellipsis)),
],
);
}
}
class _StatusChip extends StatelessWidget {
final String label;
final Color color;
final AlignmentGeometry alignment;
const _StatusChip({
required this.label,
required this.color,
this.alignment = Alignment.centerLeft,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment,
child: DecoratedBox(
decoration: BoxDecoration(
color: color.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: color.withValues(alpha: 0.45)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
),
);
}
}
class _PriorityBadge extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
const _PriorityBadge({
required this.label,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 6),
Text(label, style: TextStyle(color: color, fontWeight: FontWeight.w600)),
],
);
}
}
class _ProgressCell extends StatelessWidget {
final int percent;
const _ProgressCell(this.percent);
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 72,
child: LinearProgressIndicator(
value: percent / 100,
minHeight: 7,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 8),
Text('$percent%'),
],
);
}
}
class _HeaderLabel extends StatelessWidget {
final IconData icon;
final String text;
const _HeaderLabel({
required this.icon,
required this.text,
});
@override
Widget build(BuildContext context) {
return _IconText(
icon: icon,
text: text,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
}
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.text(
'Teammate',
width: 260,
minimumWidth: 200,
sortable: true,
filterable: true,
),
HeaderColumn.text(
'Role',
flex: 1.4,
minimumWidth: 180,
filterable: true,
),
HeaderColumn.text(
'Status',
width: 150,
minimumWidth: 120,
filterable: true,
),
HeaderColumn.text(
'Location',
flex: 1.0,
minimumWidth: 140,
filterable: true,
),
HeaderColumn.text(
'Capacity',
width: 150,
minimumWidth: 120,
sortable: true,
filterable: true,
filterType: TreeDataTableColumnFilterType.number,
),
],
),
];
final treeRowNodes = [
TreeRowNode(
columns: [
TreeColumn.custom(
widget: const _IconText(icon: Icons.person, text: 'Ava Patel'),
sortValue: 'Ava Patel',
filterValue: 'Ava Patel',
autoSizeWidth: 132,
),
TreeColumn.text('Product manager'),
TreeColumn.custom(
widget: const _StatusChip(
label: 'Available',
color: Colors.green,
alignment: Alignment.center,
),
sortValue: 'Available',
filterValue: 'Available',
autoSizeWidth: 112,
),
TreeColumn.text('Berlin'),
TreeColumn.custom(
widget: const _ProgressCell(80),
sortValue: 80,
filterValue: 80,
autoSizeWidth: 120,
),
],
),
TreeRowNode(
columns: [
TreeColumn.custom(
widget: const _IconText(icon: Icons.engineering, text: 'Noah Kim'),
sortValue: 'Noah Kim',
filterValue: 'Noah Kim',
autoSizeWidth: 132,
),
TreeColumn.text('Frontend engineer'),
TreeColumn.custom(
widget: const _StatusChip(
label: 'In review',
color: Colors.orange,
alignment: Alignment.center,
),
sortValue: 'In review',
filterValue: 'In review',
autoSizeWidth: 112,
),
TreeColumn.text('Toronto'),
TreeColumn.custom(
widget: const _ProgressCell(65),
sortValue: 65,
filterValue: 65,
autoSizeWidth: 120,
),
],
),
TreeRowNode(
columns: [
TreeColumn.custom(
widget: const _IconText(icon: Icons.analytics, text: 'Mia Santos'),
sortValue: 'Mia Santos',
filterValue: 'Mia Santos',
autoSizeWidth: 142,
),
TreeColumn.text('Data analyst'),
TreeColumn.custom(
widget: const _StatusChip(
label: 'Focused',
color: Colors.blue,
alignment: Alignment.center,
),
sortValue: 'Focused',
filterValue: 'Focused',
autoSizeWidth: 102,
),
TreeColumn.text('Lisbon'),
TreeColumn.custom(
widget: const _ProgressCell(90),
sortValue: 90,
filterValue: 90,
autoSizeWidth: 120,
),
],
),
];
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: _HeaderLabel(icon: Icons.inventory_2, text: 'Shipment note'))
: const Text('Shipment note'),
textVisibilityDialog: 'Shipment note',
width: 500,
minimumWidth: 50,
autoSize: autoSize,
autoSizeWidth: autoSizeWidth,
autoSizeWidthBuilder: autoSizeWidthBuilder,
),
HeaderColumn.text(
'Units',
width: 160,
),
],
),
],
dataSource: TreeDataTableEagerDataSource(
rows: [
TreeRowNode(
columns: [
TreeColumn.custom(
widget: scenario == ColumnAutoSizeExampleScenario.inferred
? const Text('Priority shipment with fragile glass components')
: const Text('7 cartons'),
),
TreeColumn.value(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.text(
'Catalog item',
flex: 1.4,
minimumWidth: 220,
filterable: true,
),
HeaderColumn.text(
'SKU',
width: 140,
minimumWidth: 90,
filterable: true,
),
HeaderColumn.text(
'Stock',
width: 120,
sortable: true,
),
HeaderColumn.text(
'Status',
width: 150,
minimumWidth: 90,
filterable: true,
),
],
),
];
final treeRowNodes = [
TreeRowNode(
height: 41,
columns: [
TreeColumn.custom(
widget: const _IconText(icon: Icons.kitchen, text: 'Fresh produce'),
sortValue: 'Fresh produce',
filterValue: 'Fresh produce',
),
TreeColumn.text(''),
TreeColumn.value(0, formatter: (_) => ''),
TreeColumn.text(''),
],
rows: [
TreeRowNode(
columns: [
TreeColumn.text('Honeycrisp apples'),
TreeColumn.text('PRO-APL-001'),
TreeColumn.value(48),
TreeColumn.custom(
widget: const _StatusChip(label: 'In stock', color: Colors.green),
sortValue: 'In stock',
filterValue: 'In stock',
),
],
),
TreeRowNode(
columns: [
TreeColumn.text('Organic bananas'),
TreeColumn.text('PRO-BAN-018'),
TreeColumn.value(12),
TreeColumn.custom(
widget: const _StatusChip(label: 'Low stock', color: Colors.orange),
sortValue: 'Low stock',
filterValue: 'Low stock',
),
],
),
],
),
TreeRowNode(
height: 41,
columns: [
TreeColumn.custom(
widget: const _IconText(icon: Icons.local_cafe, text: 'Beverages'),
sortValue: 'Beverages',
filterValue: 'Beverages',
),
TreeColumn.text(''),
TreeColumn.value(0, formatter: (_) => ''),
TreeColumn.text(''),
],
rows: [
TreeRowNode(
columns: [
TreeColumn.text('Sparkling water'),
TreeColumn.text('BEV-WTR-104'),
TreeColumn.value(86),
TreeColumn.custom(
widget: const _StatusChip(label: 'In stock', color: Colors.green),
sortValue: 'In stock',
filterValue: 'In stock',
),
],
),
TreeRowNode(
columns: [
TreeColumn.text('Cold brew cans'),
TreeColumn.text('BEV-CBW-220'),
TreeColumn.value(0),
TreeColumn.custom(
widget: const _StatusChip(label: 'Backorder', color: Colors.red),
sortValue: 'Backorder',
filterValue: 'Backorder',
),
],
),
],
),
];
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);
}
const _projectNames = [
'Customer portal',
'Billing migration',
'Analytics workspace',
'Mobile checkout',
'Partner API',
'Support console',
];
const _phaseNames = [
'Discovery',
'Design',
'Build',
'Validation',
'Launch',
'Operations',
];
const _owners = [
'Ava Patel',
'Noah Kim',
'Mia Santos',
'Liam Chen',
'Emma Stone',
'Omar Haddad',
'Sofia Rossi',
'Ethan Brooks',
'Priya Shah',
'Mateo Garcia',
'Hannah Weber',
'Yuki Tanaka',
'Grace Okafor',
'Lucas Martin',
'Zara Ahmed',
'Elena Petrova',
'Theo Dubois',
'Nina Kowalski',
'Jonas Meyer',
'Amara Johnson',
'Kai Andersen',
'Leila Hassan',
'Ben Carter',
'Iris Novak',
];
const _teams = [
'Platform',
'Experience',
'Data',
'Growth',
'Infrastructure',
'Operations',
];
const _statuses = [
'On track',
'In review',
'At risk',
'Blocked',
'Planned',
'Done',
];
const double _projectTreeColumnWidth = 260;
const double _projectDataColumnWidth = 170;
Color _statusColor(String status) {
return switch (status) {
'On track' => Colors.green,
'In review' => Colors.orange,
'At risk' => Colors.deepOrange,
'Blocked' => Colors.red,
'Done' => Colors.blue,
_ => Colors.blueGrey,
};
}
String _projectLabel(int index) {
return 'Initiative ${index + 1} - ${_projectNames[index % _projectNames.length]}';
}
String _phaseLabel(int level, int childIndex) {
return 'Phase $level.${childIndex + 1} - ${_phaseNames[(level + childIndex) % _phaseNames.length]}';
}
String _projectDateLabel(DateTime value) {
final month = value.month.toString().padLeft(2, '0');
final day = value.day.toString().padLeft(2, '0');
return '${value.year}-$month-$day';
}
String _projectDateTimeLabel(DateTime value) {
final hour = value.hour.toString().padLeft(2, '0');
final minute = value.minute.toString().padLeft(2, '0');
return '${_projectDateLabel(value)} $hour:$minute';
}
List<HeaderRow> _buildProjectHeaderRows() {
const groupLabels = [
('Work item', Icons.account_tree, 1),
('Ownership', Icons.groups, 3),
('Schedule', Icons.event, 1),
('Delivery', Icons.flag, 1),
('Quality', Icons.verified, 1),
('Risk profile', Icons.warning_amber, 1),
('Financials', Icons.payments, 1),
('Context', Icons.notes, 1),
];
const leafLabels = [
('Task', Icons.task_alt),
('Owner', Icons.person),
('Team', Icons.apartment),
('Status', Icons.label),
('Start', Icons.calendar_month),
('Due', Icons.event_available),
('Progress', Icons.trending_up),
('Risk', Icons.priority_high),
('Budget', Icons.attach_money),
('Notes', Icons.notes),
];
const filterableColumnIndexes = {0, 1, 2, 3, 4, 5, 7, 9};
return [
HeaderRow(
columns: [
for (var index = 0; index < groupLabels.length; index++)
HeaderColumn(
width: index == 0
? _projectTreeColumnWidth
: index == 1
? _projectDataColumnWidth * groupLabels[index].$3
: _projectDataColumnWidth,
flex: null,
minimumWidth: index == 7 ? _projectDataColumnWidth : null,
maximumWidth: index == 4 ? 250 : null,
combinedColumns: groupLabels[index].$3 > 1 ? groupLabels[index].$3 : null,
resizable: index != 5,
visible: index != 6,
widget: _HeaderLabel(icon: groupLabels[index].$2, text: groupLabels[index].$1),
textVisibilityDialog: groupLabels[index].$1,
pinLeft: index == 0,
),
],
),
HeaderRow(
columns: [
for (var index = 0; index < leafLabels.length; index++)
HeaderColumn(
width: switch (index) {
0 => _projectTreeColumnWidth,
_ => _projectDataColumnWidth,
},
flex: null,
minimumWidth: index == 0
? 180
: index == 9
? _projectDataColumnWidth
: null,
resizable: index != 7,
visible: index != 8,
canHide: index != 6,
sortable: index == 0 || index == 3 || index == 4 || index == 5 || index == 6 || index == 7 || index == 8,
filterable: filterableColumnIndexes.contains(index),
filterType: switch (index) {
4 => TreeDataTableColumnFilterType.date,
5 => TreeDataTableColumnFilterType.dateTime,
_ => TreeDataTableColumnFilterType.text,
},
widget: _HeaderLabel(icon: leafLabels[index].$2, text: leafLabels[index].$1),
textVisibilityDialog: leafLabels[index].$1,
pinLeft: index == 0,
),
],
),
];
}
class _ComplexTreeTableExampleState extends State<ComplexTreeTableExample>
with AutomaticKeepAliveClientMixin<ComplexTreeTableExample> {
late ValueNotifier<Future<_TableData>?> _tableDataFuture;
Set<Object> _selectedRowIds = const <Object>{};
@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 maxDepth = 4;
List<HeaderRow> headerRows = _buildProjectHeaderRows();
List<TreeRowNode> treeRowNodes = List.generate(
length,
(index) => _buildTreeNode(
level: 1,
maxDepth: maxDepth,
label: _projectLabel(index),
variationSeed: index,
),
);
return _TableData(headerRows, treeRowNodes);
}
static TreeRowNode _buildTreeNode({
required int level,
required int maxDepth,
required String label,
required int variationSeed,
}) {
final rowId = '$level:$variationSeed:$label';
final status = _statuses[(variationSeed + level) % _statuses.length];
final priorityIndex = (variationSeed + level * 2) % 3;
final priorityLabel = switch (priorityIndex) {
0 => 'Low',
1 => 'Medium',
_ => 'High',
};
final priorityIcon = switch (priorityIndex) {
0 => Icons.arrow_downward,
1 => Icons.drag_handle,
_ => Icons.arrow_upward,
};
final priorityColor = switch (priorityIndex) {
0 => Colors.green,
1 => Colors.orange,
_ => Colors.red,
};
final progress = (35 + variationSeed * 7 + level * 11) % 101;
final owner = _owners[(variationSeed + level) % _owners.length];
final team = _teams[(variationSeed + level * 2) % _teams.length];
final budget = 18000 + variationSeed * 1250 + level * 2200;
final startDate = DateTime(2026, level + 1, 3 + variationSeed % 8);
final dueDateTime = DateTime(2026, level + 3, 21 + variationSeed % 5, 14 + variationSeed % 4, 30);
return TreeRowNode(
id: rowId,
columns: [
TreeColumn.custom(
widget: _IconText(
icon: level == 1 ? Icons.folder_open : Icons.subdirectory_arrow_right,
text: label,
),
sortValue: label,
filterValue: label,
autoSizeWidth: level == 1 ? 230 : 190,
),
TreeColumn.text(owner),
TreeColumn.text(team),
TreeColumn.custom(
widget: _StatusChip(label: status, color: _statusColor(status)),
sortValue: status,
filterValue: status,
autoSizeWidth: 112,
),
TreeColumn.value(startDate, formatter: (value) => _projectDateLabel(value! as DateTime)),
TreeColumn.value(dueDateTime, formatter: (value) => _projectDateTimeLabel(value! as DateTime)),
TreeColumn.custom(
widget: _ProgressCell(progress),
sortValue: progress,
filterValue: progress,
autoSizeWidth: 120,
),
TreeColumn.custom(
widget: _PriorityBadge(
label: priorityLabel,
icon: priorityIcon,
color: priorityColor,
),
sortValue: priorityIndex,
filterValue: priorityLabel,
autoSizeWidth: 102,
),
TreeColumn.value(budget, formatter: (value) => '\$${(value! as int) ~/ 1000}k'),
TreeColumn.text(
level == 1 ? 'Quarterly roadmap item with cross-team visibility' : 'Child deliverable tracked separately',
),
],
rows: level < maxDepth
? List.generate(
2,
(childIdx) => _buildTreeNode(
level: level + 1,
maxDepth: maxDepth,
label: _phaseLabel(level + 1, childIdx),
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(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 12, top: 12, bottom: 12),
child: Text('Selected project rows: ${_selectedRowIds.length}'),
),
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],
),
rowSelectionMode: TreeDataTableRowSelectionMode.multiple,
selectedRowIds: _selectedRowIds,
showSelectionCheckboxes: true,
onRowSelectionChanged: (selectedRowIds) {
setState(() {
_selectedRowIds = selectedRowIds;
});
},
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 _ProjectRowData {
final int level;
final int variationSeed;
final String label;
const _ProjectRowData({
required this.level,
required this.variationSeed,
required this.label,
});
factory _ProjectRowData.root(int index) {
return _ProjectRowData(
level: 1,
variationSeed: index,
label: _projectLabel(index),
);
}
factory _ProjectRowData.child(_ProjectRowData parent, int childIndex) {
final level = parent.level + 1;
return _ProjectRowData(
level: level,
variationSeed: parent.variationSeed * 2 + childIndex + 1,
label: _phaseLabel(level, childIndex),
);
}
static _ProjectRowData? fromJson(Object? json) {
if (json is! Map) {
return null;
}
final level = json['level'];
final variationSeed = json['variationSeed'];
final label = json['label'];
if (level is! int || variationSeed is! int) {
return null;
}
return _ProjectRowData(
level: level,
variationSeed: variationSeed,
label: label is String
? label
: level == 1
? _projectLabel(variationSeed)
: _phaseLabel(level, variationSeed),
);
}
Object valueAt(int columnIndex) {
final status = _statuses[(variationSeed + level) % _statuses.length];
final priorityIndex = (variationSeed + level * 2) % 3;
final priorityLabel = switch (priorityIndex) {
0 => 'Low',
1 => 'Medium',
_ => 'High',
};
final progress = (35 + variationSeed * 7 + level * 11) % 101;
final owner = _owners[(variationSeed + level) % _owners.length];
final team = _teams[(variationSeed + level * 2) % _teams.length];
final budget = 18000 + variationSeed * 1250 + level * 2200;
final startDate = DateTime(2026, level + 1, 3 + variationSeed % 8);
final dueDateTime = DateTime(2026, level + 3, 21 + variationSeed % 5, 14 + variationSeed % 4, 30);
return switch (columnIndex) {
0 => label,
1 => owner,
2 => team,
3 => status,
4 => startDate,
5 => dueDateTime,
6 => progress,
7 => priorityLabel,
8 => budget,
9 => level == 1 ? 'Server-backed root row; children load on demand' : 'Lazy child row cached after expansion',
_ => '',
};
}
Map<String, Object?> toJson() {
return {
'level': level,
'variationSeed': variationSeed,
'label': label,
};
}
}
class _LazyComplexPageRequest {
final int lengthTableData;
final Map<String, Object?> query;
final bool serverRunning;
const _LazyComplexPageRequest({
required this.lengthTableData,
required this.query,
required this.serverRunning,
});
}
Map<String, Object?> _queryLazyComplexPage(_LazyComplexPageRequest request) {
final query = request.query;
final pageIndex = query['pageIndex']! as int;
final pageSize = query['pageSize']! as int;
final sort = query['sort'] as Map<String, Object?>?;
final filters = query['filters'] as Map<String, Object?>?;
final start = pageIndex * pageSize;
final pageRows = <_ProjectRowData>[];
var totalRowCount = 0;
if (sort == null) {
for (var index = 0; index < request.lengthTableData; index++) {
final row = _ProjectRowData.root(index);
if (!_lazyComplexMatchesFilters(row, filters)) {
continue;
}
if (totalRowCount >= start && pageRows.length < pageSize) {
pageRows.add(row);
}
totalRowCount++;
}
} else {
final rows = <_ProjectRowData>[];
for (var index = 0; index < request.lengthTableData; index++) {
final row = _ProjectRowData.root(index);
if (_lazyComplexMatchesFilters(row, filters)) {
rows.add(row);
}
}
final columnIndex = sort['columnIndex']! as int;
final descending = sort['direction'] == 'descending';
final indexedRows = rows.indexed.toList();
indexedRows.sort((a, b) {
final comparison = _lazyComplexCompareValues(
a.$2.valueAt(columnIndex),
b.$2.valueAt(columnIndex),
);
return comparison == 0 ? a.$1.compareTo(b.$1) : (descending ? -comparison : comparison);
});
totalRowCount = rows.length;
pageRows.addAll(
indexedRows.skip(start).take(pageSize).map((entry) => entry.$2),
);
}
return {
'rows': pageRows.map((row) => row.toJson()).toList(growable: false),
'totalRowCount': totalRowCount,
'serverSideMetadata': {
'transport': request.serverRunning ? 'dart-io' : 'local',
},
};
}
bool _lazyComplexMatchesFilters(_ProjectRowData row, Map<String, Object?>? filters) {
if (filters == null) {
return true;
}
final quickFilter = (filters['quickFilter'] as String?)?.trim().toLowerCase();
if (quickFilter != null &&
quickFilter.isNotEmpty &&
!List<int>.generate(10, (index) => index)
.any((columnIndex) => row.valueAt(columnIndex).toString().toLowerCase().contains(quickFilter))) {
return false;
}
final columnFilters = filters['columnFilters'] as Map<String, Object?>? ?? const {};
for (final entry in columnFilters.entries) {
final columnIndex = int.parse(entry.key);
final filter = entry.value! as Map<String, Object?>;
if (!_lazyComplexMatchesColumnFilter(row.valueAt(columnIndex), filter)) {
return false;
}
}
return true;
}
bool _lazyComplexMatchesColumnFilter(Object value, Map<String, Object?> filter) {
return switch (filter['kind']) {
'text' => _lazyComplexMatchesTextFilter(value.toString(), filter),
'number' => _lazyComplexMatchesNumberFilter(value, filter),
'date' || 'dateTime' => _lazyComplexMatchesTextFilter(value.toString(), filter),
'set' => ((filter['values'] as List<Object?>?) ?? const []).contains(value),
_ => true,
};
}
bool _lazyComplexMatchesTextFilter(String rawValue, Map<String, Object?> filter) {
final caseSensitive = filter['caseSensitive'] == true;
final haystack = caseSensitive ? rawValue : rawValue.toLowerCase();
final rawNeedle = (filter['value'] as String? ?? '').trim();
final needle = caseSensitive ? rawNeedle : rawNeedle.toLowerCase();
return switch (filter['type']) {
'notContains' => !haystack.contains(needle),
'equals' => haystack == needle,
'notEqual' => haystack != needle,
'startsWith' => haystack.startsWith(needle),
'endsWith' => haystack.endsWith(needle),
'blank' => rawValue.trim().isEmpty,
'notBlank' => rawValue.trim().isNotEmpty,
_ => haystack.contains(needle),
};
}
bool _lazyComplexMatchesNumberFilter(Object value, Map<String, Object?> filter) {
final rawTarget = filter['value'];
final rawMax = filter['maxValue'];
final target = rawTarget is num ? rawTarget : null;
final max = rawMax is num ? rawMax : null;
if (value is! num) {
return filter['type'] == 'blank';
}
return switch (filter['type']) {
'notEqual' => value != target,
'greaterThan' => target != null && value > target,
'greaterThanOrEqual' => target != null && value >= target,
'lessThan' => target != null && value < target,
'lessThanOrEqual' => target != null && value <= target,
'between' => target != null && max != null && value >= target && value <= max,
'blank' => false,
'notBlank' => true,
_ => value == target,
};
}
int _lazyComplexCompareValues(Object a, Object b) {
if (a is num && b is num) {
return a.compareTo(b);
}
if (a is DateTime && b is DateTime) {
return a.compareTo(b);
}
return a.toString().compareTo(b.toString());
}
class _LazyComplexTreeTableExampleState extends State<LazyComplexTreeTableExample>
with AutomaticKeepAliveClientMixin<LazyComplexTreeTableExample> {
static const int _maxDepth = 4;
static const String _queryPath = '/query';
static const String _childrenPath = '/children';
LazyProjectServer? _server;
LazyProjectServerClient? _serverClient;
int _serverCacheVersion = 0;
bool _serverStartComplete = false;
@override
void initState() {
super.initState();
unawaited(_startServer());
}
Future<void> _startServer() async {
LazyProjectServer? server;
try {
server = await startLazyProjectServer(_handleServerRequest);
} catch (_) {
server = null;
}
if (!mounted) {
await server?.close();
return;
}
setState(() {
_server = server;
_serverClient = createLazyProjectServerClient(server?.origin);
_serverStartComplete = true;
_serverCacheVersion++;
});
}
@override
void dispose() {
_serverClient?.close();
unawaited(_server?.close());
super.dispose();
}
Future<TreeDataTablePage> _loadPage(int pageIndex, int pageSize) async {
return _loadQueryPage(
TreeDataTablePageQuery(
pageIndex: pageIndex,
pageSize: pageSize,
),
);
}
Future<TreeDataTablePage> _loadQueryPage(TreeDataTablePageQuery query) async {
final client = _serverClient;
if (client == null) {
return _queryLocalPage(query);
}
final result = await client.postJson(_queryPath, _encodePageQuery(query));
return _decodePage(result);
}
Future<List<TreeRowNode>> _loadQueryChildren(TreeDataTableChildQuery query) async {
final client = _serverClient;
if (client == null) {
return _queryLocalChildren(query);
}
final result = await client.postJson(_childrenPath, _encodeChildQuery(query));
final rows = result['rows']! as List<Object?>;
return rows.map(_decodeTreeNodeJson).toList(growable: false);
}
Future<Map<String, Object?>> _handleServerRequest(LazyProjectServerRequest request) async {
return switch (request.path) {
_queryPath => _queryPageJson(request.body),
_childrenPath => {'rows': _queryChildrenJson(request.body)},
_ => {'error': 'Unknown lazy project endpoint: ${request.path}'},
};
}
Future<TreeDataTablePage> _queryLocalPage(TreeDataTablePageQuery query) async {
return _decodePage(await _queryPageJson(_encodePageQuery(query)));
}
List<TreeRowNode> _queryLocalChildren(TreeDataTableChildQuery query) {
return _queryChildrenJson(_encodeChildQuery(query)).map(_decodeTreeNode).toList(growable: false);
}
Future<Map<String, Object?>> _queryPageJson(Map<String, Object?> query) {
return compute(
_queryLazyComplexPage,
_LazyComplexPageRequest(
lengthTableData: widget.lengthTableData,
query: query,
serverRunning: _server != null,
),
);
}
List<Map<String, Object?>> _queryChildrenJson(Map<String, Object?> query) {
final parent = _ProjectRowData.fromJson(query['parentId']);
if (parent == null || parent.level >= _maxDepth) {
return const [];
}
return List.generate(
2,
(childIndex) => _ProjectRowData.child(parent, childIndex).toJson(),
growable: false,
);
}
TreeDataTablePage _decodePage(Map<String, Object?> json) {
final rows = json['rows']! as List<Object?>;
return TreeDataTablePage(
rows: rows.map(_decodeTreeNodeJson).toList(growable: false),
totalRowCount: json['totalRowCount']! as int,
);
}
TreeRowNode _decodeTreeNodeJson(Object? json) {
return _decodeTreeNode(Map<String, Object?>.from(json! as Map));
}
TreeRowNode _decodeTreeNode(Map<String, Object?> json) {
final row = _ProjectRowData.fromJson(json)!;
return _buildTreeNode(
level: row.level,
label: row.label,
variationSeed: row.variationSeed,
);
}
Map<String, Object?> _encodePageQuery(TreeDataTablePageQuery query) {
return {
'pageIndex': query.pageIndex,
'pageSize': query.pageSize,
'sort': query.sortColumnIndex == null || query.sortDirection == null
? null
: {
'columnIndex': query.sortColumnIndex,
'direction': query.sortDirection!.name,
},
'filters': _encodeFilters(query.filterState),
};
}
Map<String, Object?> _encodeChildQuery(TreeDataTableChildQuery query) {
return {
'parentId': _encodeRowId(query.parentId),
'sort': query.sortColumnIndex == null || query.sortDirection == null
? null
: {
'columnIndex': query.sortColumnIndex,
'direction': query.sortDirection!.name,
},
'filters': _encodeFilters(query.filterState),
'expandedRowIds': query.expandedRowIds.map(_encodeRowId).toList(),
};
}
Object? _encodeRowId(Object? id) {
return switch (id) {
_LazyComplexTreeNodeId() => {
'level': id.level,
'variationSeed': id.variationSeed,
},
_ => id,
};
}
Map<String, Object?> _encodeFilters(TreeDataTableFilterState filterState) {
return {
'quickFilter': filterState.quickFilter,
'columnFilters': {
for (final entry in filterState.columnFilters.entries) entry.key.toString(): _encodeFilter(entry.value),
},
};
}
Map<String, Object?> _encodeFilter(TreeDataTableColumnFilter filter) {
return switch (filter) {
TreeDataTableTextFilter() => {
'kind': 'text',
'type': filter.type.name,
'value': filter.text,
'caseSensitive': filter.caseSensitive,
},
TreeDataTableNumberFilter() => {
'kind': 'number',
'type': filter.type.name,
'value': filter.value,
'maxValue': filter.maxValue,
},
TreeDataTableDateFilter() => {
'kind': 'date',
'type': filter.type.name,
'value': filter.date?.toIso8601String(),
'maxValue': filter.endDate?.toIso8601String(),
},
TreeDataTableDateTimeFilter() => {
'kind': 'dateTime',
'type': filter.type.name,
'value': filter.dateTime?.toIso8601String(),
'maxValue': filter.endDateTime?.toIso8601String(),
},
TreeDataTableSetFilter() => {
'kind': 'set',
'values': filter.values.toList(),
'activeWhenEmpty': filter.activeWhenEmpty,
},
TreeDataTableCustomFilter() => {
'kind': 'custom',
},
};
}
static TreeRowNode _buildTreeNode({
required int level,
required String label,
required int variationSeed,
}) {
final status = _statuses[(variationSeed + level) % _statuses.length];
final priorityIndex = (variationSeed + level * 2) % 3;
final priorityLabel = switch (priorityIndex) {
0 => 'Low',
1 => 'Medium',
_ => 'High',
};
final priorityIcon = switch (priorityIndex) {
0 => Icons.arrow_downward,
1 => Icons.drag_handle,
_ => Icons.arrow_upward,
};
final priorityColor = switch (priorityIndex) {
0 => Colors.green,
1 => Colors.orange,
_ => Colors.red,
};
final progress = (35 + variationSeed * 7 + level * 11) % 101;
final owner = _owners[(variationSeed + level) % _owners.length];
final team = _teams[(variationSeed + level * 2) % _teams.length];
final budget = 18000 + variationSeed * 1250 + level * 2200;
final startDate = DateTime(2026, level + 1, 3 + variationSeed % 8);
final dueDateTime = DateTime(2026, level + 3, 21 + variationSeed % 5, 14 + variationSeed % 4, 30);
return TreeRowNode(
id: _LazyComplexTreeNodeId(
level: level,
variationSeed: variationSeed,
),
columns: [
TreeColumn.custom(
widget: _IconText(
icon: level == 1 ? Icons.cloud_queue : Icons.subdirectory_arrow_right,
text: label,
),
sortValue: label,
filterValue: label,
autoSizeWidth: level == 1 ? 230 : 190,
),
TreeColumn.text(owner),
TreeColumn.text(team),
TreeColumn.custom(
widget: _StatusChip(label: status, color: _statusColor(status)),
sortValue: status,
filterValue: status,
autoSizeWidth: 112,
),
TreeColumn.value(startDate, formatter: (value) => _projectDateLabel(value! as DateTime)),
TreeColumn.value(dueDateTime, formatter: (value) => _projectDateTimeLabel(value! as DateTime)),
TreeColumn.custom(
widget: _ProgressCell(progress),
sortValue: progress,
filterValue: progress,
autoSizeWidth: 120,
),
TreeColumn.custom(
widget: _PriorityBadge(
label: priorityLabel,
icon: priorityIcon,
color: priorityColor,
),
sortValue: priorityIndex,
filterValue: priorityLabel,
autoSizeWidth: 102,
),
TreeColumn.value(budget, formatter: (value) => '\$${(value! as int) ~/ 1000}k'),
TreeColumn.text(
level == 1 ? 'Server-backed root row; children load on demand' : 'Lazy child row cached after expansion',
),
],
hasChildren: level < _maxDepth,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
if (!_serverStartComplete) {
return const Center(child: CircularProgressIndicator());
}
return Center(
child: SizedBox(
height: widget.heightTable.toDouble(),
width: 1010,
child: TreeDataTable.lazy(
headerPinned: true,
headerMinExtent: 102,
headerMaxExtent: 102,
headerRows: _buildProjectHeaderRows(),
dataSource: TreeDataTableLazyDataSource(
pageLoader: _loadPage,
queryPageLoader: _loadQueryPage,
queryChildrenLoader: _loadQueryChildren,
childCacheKey: _serverCacheVersion,
cancellableLoadingBuilder: (_, cancel) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 12),
const Text('Loading server rows...'),
const SizedBox(height: 8),
TextButton(
onPressed: cancel,
child: const Text('Cancel'),
),
],
),
),
rowsPerPage: const [5, 10, 25, 100, 500],
maxLevel: _maxDepth - 1,
),
persistColumnStateId: 'lazy_complex_tree_table_example',
),
),
);
}
@override
bool get wantKeepAlive => true;
}