legacy_tree_grid 0.14.1 copy "legacy_tree_grid: ^0.14.1" to clipboard
legacy_tree_grid: ^0.14.1 copied to clipboard

Display, filter, sort, select, and edit tabular data in a tree grid.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:legacy_tree_grid/legacy_tree_grid.dart';
import 'package:legacy_context_menu/legacy_context_menu.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Legacy Tree Grid Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.light),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<UnifiedDataGridState> _gridKey = GlobalKey<UnifiedDataGridState>();

  List<Map<String, dynamic>> _data = [];
  Set<String> _selectedIds = {};
  final Set<String> _expandedIds = {};
  bool _isLoadingView = true;
  GridViewState? _savedViewState;

  // Settings
  bool _isDocked = true;
  int _currentGridTypeIndex = 0; // 0: Unified, 1: Client, 2: Server, 3: Custom
  int _currentExampleIndex = 0; // 0: Tree, 1: Flat
  bool _allowFiltering = true;
  bool _allowSorting = true;
  bool _allowColumnResize = true;
  bool _showCheckboxColumn = true;
  bool _showFooter = true;
  double _scale = 1.0;

  @override
  void initState() {
    super.initState();
    _loadInitialData();
    _loadSavedView();
  }

  void _loadInitialData() {
    if (_currentExampleIndex == 0) {
      _data = [
        {'id': '1', 'name': 'Project Alpha', 'age': 45, 'status': 'active', 'parent': null},
        {'id': '2', 'name': 'Task 1.1', 'age': 25, 'status': 'active', 'parent': '1'},
        {'id': '3', 'name': 'Task 1.2', 'age': 30, 'status': 'pending', 'parent': '1'},
        {'id': '4', 'name': 'Subtask 1.2.1', 'age': 20, 'status': 'active', 'parent': '3'},
        {'id': '5', 'name': 'Project Beta', 'age': 50, 'status': 'inactive', 'parent': null},
        {'id': '6', 'name': 'Task 2.1', 'age': 35, 'status': 'active', 'parent': '5'},
      ];
    } else {
      _data = List.generate(
        100,
        (i) => {
          'id': (i + 1).toString(),
          'name': 'User ${i + 1}',
          'age': 20 + (i % 40),
          'status': i % 3 == 0 ? 'active' : (i % 3 == 1 ? 'pending' : 'inactive'),
          'parent': null,
        },
      );
    }
  }

  Future<void> _loadSavedView() async {
    final prefs = await SharedPreferences.getInstance();
    final savedJson = prefs.getString('my_grid_view');
    setState(() {
      if (savedJson != null) {
        _savedViewState = GridViewState.fromJsonString(savedJson);
      }
      _isLoadingView = false;
    });
  }

  Future<void> _saveView() async {
    final currentState = _gridKey.currentState?.getCurrentViewState();
    if (currentState != null) {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('my_grid_view', currentState.toJsonString());
      setState(() => _savedViewState = currentState);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('View saved!'), backgroundColor: Colors.green));
      }
    }
  }

  void _restoreView() {
    if (_savedViewState != null) {
      _gridKey.currentState?.applyViewState(_savedViewState!);
    }
  }

  Future<void> _clearSavedView() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('my_grid_view');
    setState(() => _savedViewState = null);
  }

  // Mock Server Fetch
  Future<PaginatedDataResponse<Map<String, dynamic>>> _mockServerFetch(DataGridFetchOptions options) async {
    await Future.delayed(const Duration(milliseconds: 500));

    // Tree Logic: Filter only visible nodes (roots + expanded children)
    // In a real server, you'd probably return a slice of the visible tree.
    List<Map<String, dynamic>> treeAwareData = [];
    void addChildren(dynamic parentId) {
      final children = _data.where((d) => d['parent'] == parentId).toList();
      for (var child in children) {
        treeAwareData.add(child);
        if (options.expandedRowIds.contains(child['id'].toString())) {
          addChildren(child['id']);
        }
      }
    }

    if (_currentExampleIndex == 0) {
      // If it's a tree example
      addChildren(null);
    } else {
      treeAwareData = List.from(_data);
    }

    // Calculate hasChildren for all returned items
    final mappedData = treeAwareData.map((item) {
      final itemId = item['id'].toString();
      final hasChildren = _data.any((d) => d['parent'] == itemId);
      return {...item, 'hasChildren': hasChildren};
    }).toList();

    var filteredData = mappedData;

    // Filtering
    options.filters.forEach((colId, value) {
      filteredData = filteredData.where((row) => row[colId].toString().toLowerCase().contains(value.toLowerCase())).toList();
    });

    // Sorting
    if (options.sortBy != null) {
      filteredData.sort((a, b) {
        int cmp = a[options.sortBy].toString().compareTo(b[options.sortBy].toString());
        return options.sortAscending ? cmp : -cmp;
      });
    }

    // Pagination
    final start = (options.page - 1) * options.pageSize;
    final end = (start + options.pageSize).clamp(0, filteredData.length);
    final pagedData = filteredData.sublist(start, end);

    return PaginatedDataResponse(
      content: pagedData,
      totalElements: filteredData.length,
      totalPages: (filteredData.length / options.pageSize).ceil(),
      last: options.page >= (filteredData.length / options.pageSize).ceil(),
      first: options.page == 1,
      size: options.pageSize,
      number: options.page - 1,
      numberOfElements: pagedData.length,
      empty: pagedData.isEmpty,
    );
  }

  // Tree Processing for CustomDataTable (manual flattening)
  List<Map<String, dynamic>> _buildFlatTree(List<Map<String, dynamic>> data, Set<String> expandedIds) {
    if (_currentExampleIndex != 0) return data;
    List<Map<String, dynamic>> result = [];

    void addChildren(String? parentId, int level, bool parentVisible, bool parentExpanded) {
      final children = data.where((d) => d['parent'] == parentId).toList();
      for (var child in children) {
        final id = child['id'].toString();
        final hasChildren = data.any((d) => d['parent'] == id);
        final expanded = expandedIds.contains(id);

        final processed = Map<String, dynamic>.from(child);
        processed['_indentationLevel'] = level;
        processed['expanded'] = expanded;
        processed['hasChildren'] = hasChildren;
        processed['_isEffectivelyVisible'] = parentVisible && parentExpanded;

        result.add(processed);
        addChildren(id, level + 1, processed['_isEffectivelyVisible'], expanded);
      }
    }

    addChildren(null, 0, true, true);
    return result;
  }

  Widget _buildControlPanel() {
    return Drawer(
      child: ListView(
        children: [
          DrawerHeader(
            decoration: BoxDecoration(color: Theme.of(context).primaryColor),
            child: const Text('Grid Settings', style: TextStyle(color: Colors.white, fontSize: 24)),
          ),
          ListTile(
            title: const Text('Grid Implementation'),
            trailing: DropdownButton<int>(
              value: _currentGridTypeIndex,
              items: const [
                DropdownMenuItem(value: 0, child: Text('Unified')),
                DropdownMenuItem(value: 1, child: Text('ClientSide')),
                DropdownMenuItem(value: 2, child: Text('ServerSide')),
                DropdownMenuItem(value: 3, child: Text('CustomTable')),
              ],
              onChanged: (v) => setState(() => _currentGridTypeIndex = v!),
            ),
          ),
          ListTile(
            title: const Text('Dataset Type'),
            trailing: DropdownButton<int>(
              value: _currentExampleIndex,
              items: const [
                DropdownMenuItem(value: 0, child: Text('Tree Hierarchy')),
                DropdownMenuItem(value: 1, child: Text('Flat List')),
              ],
              onChanged: (v) {
                setState(() {
                  _currentExampleIndex = v!;
                  _loadInitialData();
                });
              },
            ),
          ),
          const Divider(),
          SwitchListTile(title: const Text('Dock Settings Drawer'), value: _isDocked, onChanged: (v) => setState(() => _isDocked = v)),
          SwitchListTile(title: const Text('Allow Filtering'), value: _allowFiltering, onChanged: (v) => setState(() => _allowFiltering = v)),
          SwitchListTile(title: const Text('Allow Sorting'), value: _allowSorting, onChanged: (v) => setState(() => _allowSorting = v)),
          SwitchListTile(title: const Text('Allow Resize'), value: _allowColumnResize, onChanged: (v) => setState(() => _allowColumnResize = v)),
          SwitchListTile(title: const Text('Show Checkboxes'), value: _showCheckboxColumn, onChanged: (v) => setState(() => _showCheckboxColumn = v)),
          SwitchListTile(title: const Text('Show Footer'), value: _showFooter, onChanged: (v) => setState(() => _showFooter = v)),
          const Divider(),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Text('Global Scale: ${_scale.toStringAsFixed(1)}'),
                Slider(value: _scale, min: 0.5, max: 2.0, onChanged: (v) => setState(() => _scale = v)),
              ],
            ),
          ),
        ],
      ),
    );
  }

  List<DataColumnDef> _buildColumnDefs() {
    return [
      DataColumnDef.reorder(),
      DataColumnDef.actions(
        itemsBuilder: (context, row) => [
          ContextMenuItem(caption: 'Edit', childContent: const Text('Edit'), onTap: () {}),
          ContextMenuItem(caption: 'Delete', childContent: const Text('Delete'), onTap: () {}),
        ],
      ),
      DataColumnDef(id: 'name', caption: 'Name', flex: 1, isNameColumn: true, filterType: FilterType.string),
      DataColumnDef(id: 'age', caption: 'Age', width: 80, filterType: FilterType.numeric),
      DataColumnDef(
        id: 'status',
        caption: 'Status',
        width: 100,
        cellBuilder: (context, raw, display, scale, row) {
          final color = display == 'active' ? Colors.green : (display == 'pending' ? Colors.orange : Colors.grey);
          return Container(
            margin: const EdgeInsets.all(4),
            decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
            alignment: Alignment.center,
            child: Text(display.toString(), style: const TextStyle(color: Colors.white, fontSize: 12)),
          );
        },
      ),
    ];
  }

  bool _isAncestor(String potentialAncestorId, String targetId) {
    try {
      var current = _data.firstWhere((d) => d['id'] == targetId);
      String? parentId = current['parent'];
      while (parentId != null) {
        if (parentId == potentialAncestorId) return true;
        current = _data.firstWhere((d) => d['id'] == parentId);
        parentId = current['parent'];
      }
    } catch (_) {}
    return false;
  }

  void _onReorder(String draggedId, String? targetId, bool isAfter) {
    if (targetId != null && (draggedId == targetId || _isAncestor(draggedId, targetId))) {
      return;
    }

    setState(() {
      final oldIdx = _data.indexWhere((d) => d['id'] == draggedId);
      if (oldIdx == -1) return;
      final item = _data.removeAt(oldIdx);
      if (targetId == null) {
        _data.add(item);
      } else {
        var newIdx = _data.indexWhere((d) => d['id'] == targetId);
        if (isAfter) newIdx++;
        _data.insert(newIdx.clamp(0, _data.length), item);
        final targetItem = _data.firstWhere((it) => it['id'] == targetId);
        item['parent'] = targetItem['parent'];
      }
    });
  }

  void _onNest(String draggedId, String targetParentId) {
    if (draggedId == targetParentId || _isAncestor(draggedId, targetParentId)) {
      return;
    }

    setState(() {
      final item = _data.firstWhere((d) => d['id'] == draggedId);
      item['parent'] = targetParentId;
      _expandedIds.add(targetParentId);
    });
    // For UnifiedDataGrid, also trigger the internal expansion
    _gridKey.currentState?.expandRow(targetParentId);
  }

  Widget _buildDataGrid() {
    final columnDefs = _buildColumnDefs();

    switch (_currentGridTypeIndex) {
      case 1: // ClientSideDataGrid
        return ClientSideDataGrid<Map<String, dynamic>>(
          key: _gridKey,
          data: _data,
          columnDefs: columnDefs,
          toMap: (i) => i,
          rowIdKey: 'id',
          headerHeight: 27,
          isTree: _currentExampleIndex == 0,
          parentIdKey: 'parent',
          showCheckboxColumn: _showCheckboxColumn,
          allowFiltering: _allowFiltering,
          allowColumnResize: _allowColumnResize,
          scale: _scale,
          onSelectionChanged: (ids) => setState(() => _selectedIds = ids),
          selectedRowIds: _selectedIds,
          onReorder: _onReorder,
          onNest: _onNest,
          showFooter: _showFooter,
        );
      case 2: // ServerSideDataGrid
        return ServerSideDataGrid<Map<String, dynamic>>(
          key: _gridKey,
          fetchData: _mockServerFetch,
          columnDefs: columnDefs,
          toMap: (i) => i,
          rowIdKey: 'id',
          headerHeight: 27,
          isTree: _currentExampleIndex == 0,
          parentIdKey: 'parent',
          showCheckboxColumn: _showCheckboxColumn,
          allowFiltering: _allowFiltering,
          allowColumnResize: _allowColumnResize,
          scale: _scale,
          onSelectionChanged: (ids) => setState(() => _selectedIds = ids),
          selectedRowIds: _selectedIds,
          onReorder: _onReorder,
          onNest: _onNest,
        );
      case 3: // CustomDataTable
        final treeRows = _buildFlatTree(_data, _expandedIds);
        final visibleRows = _currentExampleIndex == 0 ? treeRows.where((r) => r['_isEffectivelyVisible'] == true).toList() : _data;

        return CustomDataTable(
          columns: columnDefs,
          rows: visibleRows,
          rowIdKey: 'id',
          headerHeight: 27,
          isTree: _currentExampleIndex == 0,
          onToggleExpansion: (rowId) {
            setState(() {
              if (_expandedIds.contains(rowId)) {
                _expandedIds.remove(rowId);
              } else {
                _expandedIds.add(rowId);
              }
            });
          },
          indentationLevelKey: '_indentationLevel',
          isEffectivelyVisibleKey: '_isEffectivelyVisible',
          isExpandedKey: 'expanded',
          hasChildrenKey: 'hasChildren',
          showCheckboxColumn: _showCheckboxColumn,
          allowFiltering: _allowFiltering,
          allowColumnResize: _allowColumnResize,
          scale: _scale,
          onSelectionChanged: (ids) => setState(() => _selectedIds = ids),
          selectedRowIds: _selectedIds,
          onReorder: (oldIdx, newIdx) {
            setState(() {
              final item = _data.removeAt(oldIdx);
              _data.insert(newIdx.clamp(0, _data.length), item);
            });
          },
          onNest: (dragId, targetId) => _onNest(dragId, targetId),
        );
      case 0:
      default: // UnifiedDataGrid
        return UnifiedDataGrid<Map<String, dynamic>>(
          key: _gridKey,
          mode: DataGridMode.client,
          clientData: _data,
          columnDefs: columnDefs,
          toMap: (item) => item,
          headerHeight: 27,
          rowIdKey: 'id',
          isTree: _currentExampleIndex == 0,
          parentIdKey: 'parent',
          showCheckboxColumn: _showCheckboxColumn,
          allowFiltering: _allowFiltering,
          allowColumnResize: _allowColumnResize,
          scale: _scale,
          onSelectionChanged: (ids) => setState(() => _selectedIds = ids),
          selectedRowIds: _selectedIds,
          onReorder: _onReorder,
          onNest: _onNest,
          showFooter: _showFooter,
        );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_selectedIds.isEmpty ? 'Tree Grid Demo' : 'Selected: ${_selectedIds.length}'),
        actions: [
          if (_currentGridTypeIndex == 0) ...[
            IconButton(icon: const Icon(Icons.save), onPressed: _saveView, tooltip: 'Save View'),
            IconButton(icon: const Icon(Icons.restore), onPressed: _restoreView, tooltip: 'Restore View'),
            IconButton(icon: const Icon(Icons.delete_sweep), onPressed: _clearSavedView, tooltip: 'Clear View'),
          ],
          IconButton(icon: Icon(_isDocked ? Icons.push_pin : Icons.push_pin_outlined), onPressed: () => setState(() => _isDocked = !_isDocked)),
        ],
      ),
      drawer: _isDocked ? null : _buildControlPanel(),
      body: Row(
        children: [
          if (_isDocked) SizedBox(width: 320, child: _buildControlPanel()),
          if (_isDocked) const VerticalDivider(width: 1, thickness: 1),
          Expanded(child: _isLoadingView ? const Center(child: CircularProgressIndicator()) : _buildDataGrid()),
        ],
      ),
    );
  }
}
3
likes
160
points
180
downloads
screenshot

Documentation

API reference

Publisher

verified publishergantt-sync.com

Weekly Downloads

Display, filter, sort, select, and edit tabular data in a tree grid.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, legacy_context_menu, provider

More

Packages that depend on legacy_tree_grid