ra_table 0.0.1
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,
),
),
],
));
}
}