ra_table 0.0.1 copy "ra_table: ^0.0.1" to clipboard
ra_table: ^0.0.1 copied to clipboard

Custom Table Package with Accordion, Tree, Freeze, Search, Sort, Selectable

example/lib/main.dart

import 'package:ra_table/ra_table.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Contact Table',
      theme: ThemeData(useMaterial3: true),
      home: const ContactTablePage(), // Demo checkbox tristate
    );
  }
}

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

  @override
  State<ContactTablePage> createState() => _ContactTablePageState();
}

class _ContactTablePageState extends State<ContactTablePage> {
  // Column index constants
  static const int _colIdxId = 0;
  static const int _colIdxName = 1;
  static const int _colIdxEmail = 2;
  static const int _colIdxPhone = 3;

  final TextEditingController _searchController = TextEditingController();
  final String _searchText = '';
  late List<RATableRowData> _persistentRowData; // Persistent data structure
  int? _sortColumnIndex; // Track sorting for UITableRowData
  bool _sortAscending = true;

  // Filter management
  final List<String> _selectedFilters = [];
  List<RATableRowData> _filteredRowData = []; // Cached filtered data

  @override
  void initState() {
    super.initState();
    _persistentRowData = _createTableRowData();
    _updateFilteredData(); // Initial filtered data
  }

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

  // Update filtered data when filters change
  void _updateFilteredData() {
    _filteredRowData = _calculateFilteredRowData();
  }

  // Sort the persistent UITableRowData
  void _sortTableData(int columnIndex) {
    if (_sortColumnIndex == columnIndex) {
      _sortAscending = !_sortAscending;
    } else {
      _sortColumnIndex = columnIndex;
      _sortAscending = true;
    }

    // Sort the persistent data
    _persistentRowData.sort((RATableRowData a, RATableRowData b) {
      return _compareTableRows(a, b, columnIndex);
    });

    // Recursively sort children within each parent
    for (final rowData in _persistentRowData) {
      _sortChildrenRecursively(rowData, columnIndex);
    }
  }

  // Helper method to compare two table rows
  int _compareTableRows(RATableRowData a, RATableRowData b, int columnIndex) {
    int comparison = 0;
    switch (columnIndex) {
      case _colIdxId:
        comparison =
            (a.cells[0].titleText ?? '').compareTo(b.cells[0].titleText ?? '');
        break;
      case _colIdxName:
        comparison =
            (a.cells[1].titleText ?? '').compareTo(b.cells[1].titleText ?? '');
        break;
      case _colIdxEmail:
        comparison =
            (a.cells[2].titleText ?? '').compareTo(b.cells[2].titleText ?? '');
        break;
      case _colIdxPhone:
        comparison =
            (a.cells[3].titleText ?? '').compareTo(b.cells[3].titleText ?? '');
        break;
      default:
        break;
    }
    return _sortAscending ? comparison : -comparison;
  }

  // Helper method to recursively sort children
  void _sortChildrenRecursively(RATableRowData rowData, int columnIndex) {
    if (rowData.children == null || rowData.children!.isEmpty) return;

    // Sort this level's children
    rowData.children!.sort((RATableRowData a, RATableRowData b) {
      return _compareTableRows(a, b, columnIndex);
    });

    // Recursively sort grandchildren
    for (final child in rowData.children!) {
      _sortChildrenRecursively(child, columnIndex);
    }
  }

  // Cached expanded content widget - created once to avoid rebuilding
  Widget get _expandedContentWidget => Card(
        color: Colors.blue[50],
        margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            'Example expanded content goes here.\nYou can put any widget here!',
            style: GoogleFonts.aBeeZee(
              fontSize: 16,
              fontWeight: FontWeight.w500,
              color: Colors.blueAccent,
            ),
          ),
        ),
      );

  // Create table row data directly
  List<RATableRowData> _createTableRowData() {
    return [
      // Alice Group with complex nested structure
      RATableRowData(
        cells: [
          const RATableCell(titleText: '123'),
          const RATableCell(titleText: 'Alice Group'),
          const RATableCell(titleText: 'alice@example.com'),
          const RATableCell(titleText: '+1-555-0101'),
        ],
        children: [
          RATableRowData(
            cells: [
              const RATableCell(titleText: '123-1'),
              const RATableCell(titleText: 'Alice Child 1'),
              const RATableCell(titleText: 'child1@example.com'),
              const RATableCell(titleText: '123-1-1'),
            ],
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '123-3'),
              const RATableCell(titleText: 'Alice Manager'),
              const RATableCell(titleText: 'manager@example.com'),
              const RATableCell(titleText: '123-1-2'),
            ],
            children: [
              RATableRowData(
                cells: [
                  const RATableCell(titleText: '123-3-1'),
                  const RATableCell(titleText: 'Sub Team A'),
                  const RATableCell(titleText: 'teama@example.com'),
                  const RATableCell(titleText: '123-1-1-1'),
                ],
                children: [
                  RATableRowData(
                    cells: [
                      const RATableCell(titleText: '123-3-1-1'),
                      const RATableCell(titleText: 'Frontend Dev 1'),
                      const RATableCell(titleText: 'dev1@example.com'),
                      const RATableCell(titleText: '123-1-1-1-1'),
                    ],
                  ),
                  RATableRowData(
                    cells: [
                      const RATableCell(titleText: '123-3-1-2'),
                      const RATableCell(titleText: 'Frontend Dev 2'),
                      const RATableCell(titleText: 'dev2@example.com'),
                      const RATableCell(titleText: '123-1-1-1-2'),
                    ],
                  ),
                ],
              ),
              RATableRowData(
                cells: [
                  const RATableCell(titleText: '123-3-2'),
                  const RATableCell(titleText: 'Sub Team B'),
                  const RATableCell(titleText: 'teamb@example.com'),
                  const RATableCell(titleText: '123-1-2-1'),
                ],
                children: [
                  RATableRowData(
                    cells: [
                      const RATableCell(titleText: '123-3-2-1'),
                      const RATableCell(titleText: 'Backend Dev 1'),
                      const RATableCell(titleText: 'backend1@example.com'),
                      const RATableCell(titleText: '123-1-2-1-1'),
                    ],
                    child: _expandedContentWidget,
                  ),
                  RATableRowData(
                    cells: [
                      const RATableCell(titleText: '123-3-2-2'),
                      const RATableCell(titleText: 'Backend Dev 2'),
                      const RATableCell(titleText: 'backend2@example.com'),
                      const RATableCell(titleText: '123-1-2-1-2'),
                    ],
                  ),
                ],
              ),
            ],
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '123-2'),
              const RATableCell(titleText: 'Alice Child 2'),
              const RATableCell(titleText: 'child2@example.com'),
              const RATableCell(titleText: '123-2-1'),
            ],
          ),
        ],
      ),
      // Bob Manager with child content
      RATableRowData(
        cells: [
          const RATableCell(titleText: '2'),
          const RATableCell(titleText: 'Bob Manager'),
          const RATableCell(titleText: 'bob@example.com'),
          const RATableCell(titleText: '+1-555-0102'),
        ],
        children: [
          RATableRowData(
            cells: [
              const RATableCell(titleText: '2-1'),
              const RATableCell(titleText: 'Bob Junior'),
              const RATableCell(titleText: 'bobjr@example.com'),
              const RATableCell(titleText: '2-1-1'),
            ],
            child: _expandedContentWidget,
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '2-2'),
              const RATableCell(titleText: 'Bob Senior'),
              const RATableCell(titleText: 'bobsr@example.com'),
              const RATableCell(titleText: '2-1-2'),
            ],
          ),
        ],
      ),
      // Charlie Director with department structure
      RATableRowData(
        cells: [
          const RATableCell(titleText: '3'),
          const RATableCell(titleText: 'Charlie Director'),
          const RATableCell(titleText: 'charlie@example.com'),
          const RATableCell(titleText: '+1-555-0103'),
        ],
        children: [
          RATableRowData(
            cells: [
              const RATableCell(titleText: '3-1'),
              const RATableCell(titleText: 'Engineering'),
              const RATableCell(titleText: 'eng@example.com'),
              const RATableCell(titleText: '3-1-1'),
            ],
            children: [
              RATableRowData(
                cells: [
                  const RATableCell(titleText: '3-1-1'),
                  const RATableCell(titleText: 'Frontend Team'),
                  const RATableCell(titleText: 'frontend@example.com'),
                  const RATableCell(titleText: '3-1-1-1'),
                ],
              ),
              RATableRowData(
                cells: [
                  const RATableCell(titleText: '3-1-2'),
                  const RATableCell(titleText: 'Backend Team'),
                  const RATableCell(titleText: 'backend@example.com'),
                  const RATableCell(titleText: '3-1-1-2'),
                ],
              ),
            ],
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '3-2'),
              const RATableCell(titleText: 'Design'),
              const RATableCell(titleText: 'design@example.com'),
              const RATableCell(titleText: '3-2-1'),
            ],
          ),
        ],
      ),
      // Diana CEO with project teams
      RATableRowData(
        cells: [
          const RATableCell(titleText: '4'),
          const RATableCell(titleText: 'Diana CEO'),
          const RATableCell(titleText: 'diana@example.com'),
          const RATableCell(titleText: '+1-555-0104'),
        ],
        children: [
          RATableRowData(
            cells: [
              const RATableCell(titleText: '4-1'),
              const RATableCell(titleText: 'Project Alpha'),
              const RATableCell(titleText: 'alpha@example.com'),
              const RATableCell(titleText: '4-1-1'),
            ],
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '4-2'),
              const RATableCell(titleText: 'Project Beta'),
              const RATableCell(titleText: 'beta@example.com'),
              const RATableCell(titleText: '4-1-2'),
            ],
          ),
          RATableRowData(
            cells: [
              const RATableCell(titleText: '4-3'),
              const RATableCell(titleText: 'Project Gamma'),
              const RATableCell(titleText: 'gamma@example.com'),
              const RATableCell(titleText: '4-1-3'),
            ],
          ),
        ],
      ),
      // Eve Developer (simple entry)
      RATableRowData(
        cells: [
          const RATableCell(titleText: '5'),
          const RATableCell(titleText: 'Eve Developer'),
          const RATableCell(titleText: 'eve@example.com'),
          const RATableCell(titleText: '+1-555-0105'),
        ],
        child: _expandedContentWidget,
      ),
    ];
  }

  // Calculate filtered rowData based on search text and selected filters
  List<RATableRowData> _calculateFilteredRowData() {
    // Apply filtering only if there are search terms or filters
    if (_searchText.isEmpty && _selectedFilters.isEmpty) {
      return _persistentRowData;
    }

    return _persistentRowData.where((rowData) {
      // Search text matching (if present)
      bool searchMatches = _searchText.isEmpty ||
          _rowContainsAnyTerm(rowData, [_searchText], requireAll: false);

      // Filter matching: Show rows that contain ANY of the selected filters (OR logic)
      bool filtersMatch = _selectedFilters.isEmpty ||
          _rowContainsAnyTerm(rowData, _selectedFilters, requireAll: false);

      return searchMatches && filtersMatch;
    }).toList();
  }

  // Unified function to check if row data contains any of the given search terms
  // This handles both search text and filter matching with recursive children search
  bool _rowContainsAnyTerm(RATableRowData rowData, List<String> searchTerms,
      {bool requireAll = false}) {
    if (searchTerms.isEmpty) return true;

    int matchCount = 0;

    for (final term in searchTerms) {
      bool termMatches = _rowAndChildrenContainTerm(rowData, term);

      if (termMatches) {
        matchCount++;
        // If we only need one match (OR logic), return early
        if (!requireAll) return true;
      } else if (requireAll) {
        // If we need all matches (AND logic) and one fails, return early
        return false;
      }
    }

    // For requireAll=true, check if all terms matched
    // For requireAll=false, check if any terms matched
    return requireAll ? matchCount == searchTerms.length : matchCount > 0;
  }

  // Recursively check if row or its children contain the search term
  bool _rowAndChildrenContainTerm(RATableRowData rowData, String term) {
    final lowerTerm = term.toLowerCase();

    // Check current row's cells
    bool currentRowMatches = rowData.cells.any(
        (cell) => cell.titleText?.toLowerCase().contains(lowerTerm) == true);

    if (currentRowMatches) return true;

    // Check children recursively
    if (rowData.children != null) {
      for (final child in rowData.children!) {
        if (_rowAndChildrenContainTerm(child, term)) {
          return true;
        }
      }
    }

    return false;
  }

  // Table column definitions - static to avoid recreating
  static const List<RATableColumn> _columns = [
    RATableColumn(
      titleText: 'ID',
      sortable: true,
    ),
    RATableColumn(
      titleText: 'Name',
      sortable: true,
    ),
    RATableColumn(
      titleText: 'Email',
      sortable: true,
    ),
    RATableColumn(
      titleText: 'Phone',
      sortable: true,
    ),
  ];

  // Column width definitions - static to avoid recreating
  static const Map<int, double> _columnWidths = {
    0: 180.0, // ID column
    1: 200.0, // Name column
    2: 250.0, // Email column
    3: 180.0, // Phone column
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            'Example Custom Table',
            style:
                GoogleFonts.aBeeZee(fontSize: 16, fontWeight: FontWeight.w800),
          ),
        ),
        body: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Padding(
              padding: const EdgeInsets.all(16),
              child: RATable(
                rows: _filteredRowData,
                columns: _columns,
                columnWidths: _columnWidths,
                accordionStyle: const RATableAccordionStyle(
                  headColor: Colors.white,
                  iconColor: Colors.blueAccent,
                  lineColor: Colors.blueAccent,
                ),
                selectableType: RATableSelectableType.none,
                headerStyle: RATableHeaderStyle(
                  backgroundColor: Colors.white,
                  textStyle: GoogleFonts.aBeeZee(
                    fontSize: 14,
                    fontWeight: FontWeight.w800,
                    color: Colors.black,
                  ),
                  iconColor: Colors.black,
                ),
                bodyTextStyle: GoogleFonts.aBeeZee(
                  fontSize: 14,
                ),
                // freezeColumnLeft: const UITableFreeze(
                //   length: 1,
                // ),
                // freezeColumnRight: const UITableFreeze(
                //   length: 1,
                // ),
                onSort: (sortColumnIndex) {
                  _sortTableData(sortColumnIndex);
                  setState(() {
                    _sortColumnIndex = sortColumnIndex;
                    _updateFilteredData(); // Refresh filtered data after sorting
                  });
                },
                currentSortColumnIndex: _sortColumnIndex,
                sortAscending: _sortAscending,
              ),
            ),
          ],
        ));
  }
}
1
likes
140
points
7
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Custom Table Package with Accordion, Tree, Freeze, Search, Sort, Selectable

Homepage

License

MIT (license)

Dependencies

flutter

More

Packages that depend on ra_table