Skip to content

LayrzTable

Kenny Mochizuki Escalona edited this page Aug 13, 2026 · 2 revisions

LayrzTable

A data table component with dynamic columns, sorting, search, multiselect, and row actions. Supports custom cell rendering and programmatic control via a controller.

Metadata
Mirrors: ThemedTable2<T> (layrz_theme)
Phase: M6 (Data display)
Domain: Data
Primitive: Depends on architecture decision (see below)
Status: Specification status DERIVED from layrz_theme, not yet confirmed by the team.


Overview

LayrzTable<T> is a generic data table widget for displaying and interacting with collections of domain objects. It supports:

  • Dynamic column definitions with custom rendering, sorting, and alignment.
  • Full-text search across all columns with debounced filtering.
  • Multi-select mode with checkboxes and batch actions.
  • Row actions — custom buttons per row for drill-down, edit, or delete workflows.
  • Sorting — click column headers to sort ascending/descending; custom sort comparators per column.
  • Responsive layout — horizontal scroll on narrow viewports, actions drawer on mobile.
  • Programmatic controlLayrzTableController<T> for external sorting, searching, and refresh.

Design Principles

  • Generic over domain typeLayrzTable<T> where T is your domain object (User, Device, Order, etc.).
  • Column-centric definition — Columns are declared once; rendering, sorting, and tap behavior flow from column metadata.
  • Search-as-you-type — Debounced full-text search across all cell values; no explicit "go" button.
  • Multiselect as opt-in — Enable or disable via constructor parameter; state lives in a ValueNotifier.
  • Controller-driven events — Programmatic sorting, searching, and refresh via discrete event types.
  • Responsive surface — Rows built on LayrzCard (link: LayrzCard) for consistent elevation and hover treatment.

API Structure

Core Widget

// Design sketch — illustrative only
class LayrzTable<T> extends StatefulWidget {
  /// List of domain objects to display.
  final List<T> items;

  /// Column definitions (headers, rendering, sorting, alignment).
  final List<LayrzColumn<T>> columns;

  /// Optional builder for action buttons per row.
  /// If provided, [actionsCount] must be > 0.
  final List<LayrzActionButton> Function(T item)? actionsBuilder;

  /// Maximum number of actions per row.
  /// If 0, actions column is omitted.
  final int actionsCount;

  /// Column label for the actions column.
  final String actionsLabelText;

  /// Breakpoint (logical pixels) at which actions switch to mobile layout.
  final double actionsMobileBreakpoint;

  /// Height of the table header row.
  final double headerHeight;

  /// Enable or disable the multiselect checkbox column.
  final bool hasMultiselect;

  /// Label text shown while the table is loading or computing.
  final String loadingLabelText;

  /// Enable or disable the search field above the table.
  final bool canSearch;

  /// Minimum width for flexible columns.
  final double minColumnWidth;

  /// Multiselect state notifier (List<T> of selected items).
  /// Only used if [hasMultiselect] is true.
  final ValueNotifier<List<T>>? multiselectValue;

  /// Action buttons shown when 1+ items are selected.
  /// Automatically appended with a "clear selection" button.
  final List<LayrzActionButton> multiselectActions;

  /// Programmatic control: sorting, searching, refresh.
  final LayrzTableController<T>? controller;

  /// Callback when filtered row count changes (after search/sort).
  final void Function(int count)? onFilteredCountChanged;

  /// Default behavior when a cell is tapped with no explicit tap handler.
  /// Enum: none, copyToClipboard.
  final LayrzTableOnTapBehavior onTapDefaultBehavior;

  /// Copy-to-clipboard confirmation text.
  /// Defaults to locale or fallback text.
  final String? copyToClipboardText;

  /// Delay before populating the table with initial data.
  final Duration populateDelay;

  // Constructor omitted for brevity
}

Column Definition

// Design sketch — illustrative only
class LayrzColumn<T> {
  /// Header text displayed in the column header.
  final String headerText;

  /// Function to extract a string value from the item for the cell.
  /// Used for sorting and search.
  final String Function(T item) valueBuilder;

  /// Optional rich text builder for cell rendering.
  /// Returns a list of InlineSpan for styled rendering (e.g., bold, colors, icons).
  /// If not supplied, [valueBuilder] is rendered as plain text.
  final List<InlineSpan> Function(T item)? richTextBuilder;

  /// Cell alignment (left, center, right, etc.).
  final Alignment alignment;

  /// Enable or disable sorting for this column.
  final bool isSortable;

  /// Optional fixed width for this column.
  /// If null, width is calculated flexibly based on available space.
  final double? width;

  /// Callback when this cell is tapped.
  /// Receives the item and its index.
  /// If null, the default behavior ([onTapDefaultBehavior]) is applied.
  final void Function(T item, int index)? onTap;

  /// Custom sort comparator for this column.
  /// Receives two items and an ascending flag; returns <0, 0, or >0.
  /// If null, sorting is lexicographic on [valueBuilder] result.
  final int Function(T a, T b, bool ascending)? customSort;

  // Constructor omitted for brevity
}

Controller

// Design sketch — illustrative only
class LayrzTableController<T> {
  /// Register a listener to receive table events.
  void addListener(void Function(LayrzTableEvent event) listener);

  /// Remove a registered listener.
  void removeListener(void Function(LayrzTableEvent event) listener);

  /// Clear all listeners.
  void clearListeners();

  /// Programmatically trigger a sort event.
  /// [columnIndex] is the zero-based position in the columns list.
  /// [ascending] determines sort order.
  void sort({int columnIndex = 0, bool ascending = true});

  /// Notify the table that external sorting has been applied (e.g., by a backend).
  void onSort({int columnIndex = 0, bool ascending = true});

  /// Programmatically trigger a search.
  void search(String searchQuery);

  /// Notify the table that external search has been applied.
  void onSearch(String searchQuery);

  /// Trigger a refresh (e.g., reload data from an external source).
  void refresh();

  /// Dispose of the controller (clears all listeners).
  void dispose();
}

Events

// Design sketch — illustrative only
abstract class LayrzTableEvent<T> {}

class LayrzTableSortEvent<T> extends LayrzTableEvent<T> {
  final int columnIndex;
  final bool ascending;
  // Constructor omitted
}

class LayrzTableOnSortEvent<T> extends LayrzTableEvent<T> {
  final int columnIndex;
  final bool ascending;
  // Constructor omitted
}

class LayrzTableSearchEvent<T> extends LayrzTableEvent<T> {
  final String search;
  // Constructor omitted
}

class LayrzTableOnSearchEvent<T> extends LayrzTableEvent<T> {
  final String search;
  // Constructor omitted
}

class LayrzTableRefreshEvent<T> extends LayrzTableEvent<T> {}

OnTapBehavior Enum

// Design sketch — illustrative only
enum LayrzTableOnTapBehavior {
  /// No action when a cell is tapped (unless column has explicit [onTap]).
  none,

  /// Copy the cell content to the clipboard.
  copyToClipboard,
}

Column Declaration and Rendering

Columns are declared as a static list of LayrzColumn<T> objects:

final columns = [
  LayrzColumn<User>(
    headerText: 'Name',
    valueBuilder: (user) => user.name,
    alignment: Alignment.centerLeft,
    isSortable: true,
  ),
  LayrzColumn<User>(
    headerText: 'Email',
    valueBuilder: (user) => user.email,
    richTextBuilder: (user) => [
      TextSpan(
        text: user.email,
        style: TextStyle(color: Colors.blue),
      ),
    ],
    onTap: (user, index) => print('Tapped: ${user.email}'),
  ),
  LayrzColumn<User>(
    headerText: 'Status',
    valueBuilder: (user) => user.isActive ? 'Active' : 'Inactive',
    customSort: (a, b, ascending) {
      // Custom sort: active users first
      int result = (a.isActive ? 0 : 1).compareTo(b.isActive ? 0 : 1);
      return ascending ? result : -result;
    },
  ),
];

Sorting and Searching

Built-in Search

If canSearch = true, a search field appears above the table. It triggers LayrzTableSearchEvent as the user types (debounced). The table filters all rows by matching the search query against all cell values.

Programmatic Sort

final controller = LayrzTableController<User>();

// Trigger a sort via controller
controller.sort(columnIndex: 0, ascending: false);

// Listen for sort events
controller.addListener((event) {
  if (event is LayrzTableSortEvent) {
    // Handle: sort by column index [event.columnIndex], order [event.ascending]
    // Usually: reload data from backend or sort in-memory
  }
});

Multiselect and Row Actions

Multiselect State

final selectedUsers = ValueNotifier<List<User>>([]);

LayrzTable<User>(
  items: users,
  columns: columns,
  hasMultiselect: true,
  multiselectValue: selectedUsers,
  multiselectActions: [
    LayrzActionButton(labelText: 'Delete', onTap: () { /* ... */ }),
    LayrzActionButton(labelText: 'Export', onTap: () { /* ... */ }),
  ],
)

// Listen for changes
selectedUsers.addListener(() {
  print('Selected ${selectedUsers.value.length} users');
});

When items are selected, a toolbar appears with the supplied multiselectActions plus an auto-added "Clear" button.

Row Actions

LayrzTable<User>(
  items: users,
  columns: columns,
  actionsCount: 3,
  actionsBuilder: (user) => [
    LayrzActionButton(
      labelText: 'View',
      onTap: () => print('View ${user.name}'),
    ),
    LayrzActionButton(
      labelText: 'Edit',
      onTap: () => print('Edit ${user.name}'),
    ),
    LayrzActionButton(
      labelText: 'Delete',
      onTap: () => print('Delete ${user.name}'),
    ),
  ],
)

Each row displays up to actionsCount action buttons. On mobile (below actionsMobileBreakpoint), actions drawer into a menu.


Programmatic Control via Controller

The LayrzTableController<T> allows external sorting, searching, and refresh:

final controller = LayrzTableController<User>();

LayrzTable<User>(
  items: users,
  columns: columns,
  controller: controller,
)

// Externally trigger search (e.g., from a separate search box)
controller.search('john@example.com');

// Externally trigger sort
controller.sort(columnIndex: 1, ascending: true);

// Notify the table of a refresh
controller.refresh();

// Listen for events
controller.addListener((event) {
  if (event is LayrzTableSearchEvent) {
    // Backend search triggered; fetch results
  } else if (event is LayrzTableSortEvent) {
    // Backend sort triggered; fetch results
  }
});

Dependencies

Required Dependencies

  • M1 Theme System (LayrzTheme, LayrzThemeData) — Colors, text styles, and theme tokens for rendering.
  • M2 Display Primitive (LayrzCard) — Surface container for rows; provides elevation and hover state.
  • M2 Button Component (LayrzButton) — Action and multiselect toolbar buttons.

Key Dependencies from layrz_theme Implementation

The following dependencies are FACTS recorded from the layrz_theme source audit:

  • sync_scroll_controller 1.0.1IMPORT-COUPLED ONLY. Imports package:flutter/material.dart at line 7 but uses ZERO Material symbols. Everything it touches (ScrollController, ScrollPosition, ScrollActivity, ScrollPhysics, ChangeNotifier, Curve) comes from widgets, rendering, foundation, or dart:ui. It is a single 379-line file exposing one public class, SyncScrollControllerGroup. Open concern: SDK constraint is >=2.17.6 <3.0.0 (Dart 2 upper bound); whether it resolves under Dart 3.13 needs verifying. Reimplementation is cheap if needed (379 lines, one class).

  • two_dimensional_scrollables 0.3.9CLEAN — Zero Material or Cupertino imports; the only Colors. references are inside /// doc comments. Exposes TableView, TableViewport, TableViewCell, TableSpan, TableCellBuilderDelegate, TableCellListDelegate, TableVicinity, TableRowBorder and span types. Built on the Flutter SDK's own TwoDimensionalViewport and RenderTwoDimensionalViewport (in the SDK at lib/src/widgets/two_dimensional_viewport.dart and two_dimensional_scroll_view.dart). Directly usable by a Material-free design system.


Architecture Decision: Scrolling Foundation

This is an OPEN QUESTION, not yet decided.

The layrz_theme implementation of ThemedTable2 uses only sync_scroll_controller — it does NOT use two_dimensional_scrollables. The older, legacy ThemedTable did use two_dimensional_scrollables.

LayrzTable has a genuine architectural choice:

  1. Follow table2's approach — Use sync_scroll_controller to coordinate horizontal and vertical scrolling. Pros: proven in production. Cons: carries a Material import in the dependency graph (albeit unused); constrains to Dart 2 bounds.

  2. Build on two_dimensional_scrollables — Use the clean TableView primitives and avoid the Material import entirely. Pros: no Material import; fresh surface. Cons: different from the proven approach; may require layout rework.

  3. Go directly to SDK primitives — Use the SDK's TwoDimensionalViewport directly (which two_dimensional_scrollables wraps). Pros: zero external dependencies beyond Flutter. Cons: lower-level API; more boilerplate.

Key fact: Regardless of choice, LayrzTable's own chrome (cells, headers, actions, multiselect toolbar) must be rebuilt from scratch because ThemedTable2 itself imports package:flutter/material.dart.


Open Questions

The following are clarifications needed from the source or design team:

  • Generics and type parameter preservation — Do LayrzColumn<T>, LayrzTableController<T>, and LayrzTableEvent<T> carry the type parameter unchanged? Or does event dispatch erase the type?

  • Row action declaration and callback signature — The source uses a CellTap<T> typedef for column onTap handlers. What is the callback signature for row actions? Do they receive the item, the index, or both?

  • Multiselect state ownership — Does multiselect state live entirely in multiselectValue (a ValueNotifier<List<T>>), or does the controller manage selections?

  • Column sizing and responsive behavior — How are flexible column widths calculated when width is null? Is there a priority order, or are they split equally? How do columns respond to viewport resize?

  • Loading state — The loadingLabelText suggests the table can display a loading state. Who owns the loading flag? Is there a isLoading parameter, or is this managed by the controller/parent?

  • Search across specific columns — Does search run across all columns, or can certain columns be excluded from search?

  • Isolation safety — The source has an isolateSafety getter on columns that strips onTap and richTextBuilder. Is this for sending columns to Dart isolates? Should this be exposed in layrz_ui?

  • Tap feedback on cells — When onTapDefaultBehavior is copyToClipboard, what visual feedback is shown (toast, snackbar, tooltip)?


Design Reference

Critical path item: A design reference (Figma, annotated screenshot, or spec) must be attached to the LayrzTable component before implementation begins. This specification must cover:

  • Table header chrome (cell padding, height, text styling, sort indicator appearance).
  • Data cell styling, padding, and row height.
  • Multiselect checkbox styling and column width.
  • Actions column layout and button sizing (desktop vs. mobile).
  • Horizontal scroll affordance and behavior.
  • Loading state appearance.
  • Empty state (no items) display.
  • Search input styling and placeholder text.
  • Multiselect toolbar styling and button layout.
  • Light and dark theme variants for all states.

Last updated: 2026-08-13
Related documents: LayrzCard, Design Tokens, Architecture, Roadmap

Clone this wiki locally