build method

  1. @override
Widget build(
  1. BuildContext context
)
override

Describes the part of the user interface represented by this widget.

The framework calls this method when this widget is inserted into the tree in a given BuildContext and when the dependencies of this widget change (e.g., an InheritedWidget referenced by this widget changes). This method can potentially be called in every frame and should not have any side effects beyond building a widget.

The framework replaces the subtree below this widget with the widget returned by this method, either by updating the existing subtree or by removing the subtree and inflating a new subtree, depending on whether the widget returned by this method can update the root of the existing subtree, as determined by calling Widget.canUpdate.

Typically implementations return a newly created constellation of widgets that are configured with information from this widget's constructor and from the given BuildContext.

The given BuildContext contains information about the location in the tree at which this widget is being built. For example, the context provides the set of inherited widgets for this location in the tree. A given widget might be built with multiple different BuildContext arguments over time if the widget is moved around the tree or if the widget is inserted into the tree in multiple places at once.

The implementation of this method must only depend on:

If a widget's build method is to depend on anything else, use a StatefulWidget instead.

See also:

  • StatelessWidget, which contains the discussion on performance considerations.

Implementation

@override
Widget build(BuildContext context) {
  final effectiveTheme = theme ??
      PagedDataTableThemeData(
        rowColors: [CLTheme.of(context).secondaryBackground, CLTheme.of(context).secondaryBackground],
        border: const RoundedRectangleBorder(side: BorderSide.none),
        backgroundColor: Colors.transparent,
        headerBackgroundColor: Colors.transparent,
        filtersHeaderBackgroundColor: Colors.transparent,
        footerBackgroundColor: Colors.transparent,
        titleStyle: CLTheme.of(context).heading1,
        footerTextStyle: CLTheme.of(context).bodyLabel,
        headerTextStyle: CLTheme.of(context).bodyLabel,
        textStyle: CLTheme.of(context).bodyText,
        buttonsColor: primaryColor ?? CLTheme.of(context).primary,
        rowsTextStyle: CLTheme.of(context).bodyText,
        configuration: PagedDataTableConfiguration(
          filterBarVisibile: isFilterBarVisible,
          footer: PagedDataTableFooterConfiguration(footerVisible: isFooterVisible),
          pageSizes: pageSizes ?? [5, 25, 50, 100],
          initialPageSize: initialPageSize ?? pageSizes?.first ?? 5,
        ),
      );

  final localTheme = effectiveTheme;
  final Widget tableTree = ChangeNotifierProvider<_PagedDataTableState<TKey, TResultId, TResult>>(
    create: (context) => _PagedDataTableState(
      downloadCallback: downloadPage,
      columns: columns,
      rowsSelectable: rowsSelectable,
      showShimmerLoading: showShimmerLoading,
      mainFilter: mainFilter,
      extraFilters: extraFilters,
      idGetter: idGetter,
      controller: controller,
      fetchCallback: fetchPage,
      initialPage: initialPage,
      pageSize: localTheme.configuration.initialPageSize,
      refreshListener: refreshListener,
    ),
    builder: (context, widget) {
      var state = context.read<_PagedDataTableState<TKey, TResultId, TResult>>();
      // Update columns reference so cellBuilder closures use the latest theme
      state.columns = columns;
      // Split actions into inline (rendered as plain compact icon button in
      // the row) and popup (rendered behind the 3-dot menu).
      final inlineActions = tableActions.where((a) => a.inline).toList();
      final popupActionsCount = tableActions.where((a) => !a.inline).length;
      // Popup column shows when static popup actions exist or a dynamic
      // actionsBuilder is provided (which may yield popup actions per row).
      final hasPopupActions = popupActionsCount > 0 || actionsBuilder != null;
      // Whether rows have expand icon
      final hasExpandIcon = expandedRowBuilder != null;
      // Single source of truth: reserve == render for every leading/trailing slot.
      final m = PagedDataTableRowMetrics.of(context);
      final inlineCount = inlineActions.length;
      final double actionsColumnWidth = m.actionsColumnWidth(inlineCount: inlineCount, hasPopup: hasPopupActions);
      final double checkboxAreaWidth = m.checkboxAreaWidth;
      final double expandIconAreaWidth = m.expandIconAreaWidth;
      final hasAnyActions = hasPopupActions || inlineActions.isNotEmpty;

      Widget child = LayoutBuilder(
        builder: (context, constraints) {
          // Bolla righe: ingombro orizzontale = margin Lg per lato (niente bordo).
          final double rowsBubbleInset = 2 * Sizes.gapLg;
          // Calculate width available for columns only
          var width = constraints.maxWidth -
              rowsBubbleInset // la bolla restringe la zona righe
              -
              m.leftBorderWidth // left border in rows
              -
              (hasExpandIcon ? expandIconAreaWidth : 0) -
              (rowsSelectable ? checkboxAreaWidth : 0) -
              (hasAnyActions ? actionsColumnWidth : 0);
          state.availableWidth = width;
          // Solo fillHeight fa possedere lo scroll alla lista (Expanded +
          // physics scrollabili). Con infiniteScroll ma SENZA fillHeight la lista
          // resta shrinkWrap: è la PAGINA a scrollare e a guidare il load-more
          // (controller.loadNextPage). La tabella renderizza solo righe + loader.
          final ownScroll = fillHeight;
          // Sezione righe desktop: in fillHeight occupa lo spazio rimanente
          // e scorre da sola (header/filter bar restano fissi sopra).
          Widget rowsSection = _PagedDataTableRows<TKey, TResultId, TResult>(
            rowsSelectable,
            onItemTap,
            isInSnippet,
            customRowBuilder ??
                CustomRowBuilder<TResult>(
                  builder: (context, item) => throw UnimplementedError("This does not build nothing"),
                  shouldUse: (context, item) => false,
                ),
            noItemsFoundBuilder,
            errorBuilder,
            width,
            actionsTitle,
            tableActions,
            actionsBuilder,
            localTheme.configuration.initialPageSize,
            showShimmerLoading,
            expandedRowBuilder,
            onRowExpanded,
            ownScroll,
            hasExpandIcon,
            hasAnyActions ? actionsColumnWidth : 0.0,
            infiniteScroll,
          );
          // Bolla righe: container arrotondato, niente bordo. Margin SENZA top: il
          // margin-top si sommerebbe al centering interno della prima riga (= 2Lg).
          // Clip sul radius → zebra full-bleed rispetta gli angoli tondi.
          rowsSection = Container(
            margin: const EdgeInsets.fromLTRB(Sizes.gapLg, 0, Sizes.gapLg, Sizes.gapLg),
            clipBehavior: Clip.antiAlias,
            decoration: BoxDecoration(borderRadius: BorderRadius.circular(Sizes.radiusCard)),
            child: rowsSection,
          );
          if (ownScroll) rowsSection = Expanded(child: rowsSection);
          // Sezione card mobile: stesso trattamento.
          Widget boxedSection = _PagedDataTableBoxed<TKey, TResultId, TResult>(
            rowsSelectable,
            onItemTap,
            isInSnippet,
            customRowBuilder ??
                CustomRowBuilder<TResult>(
                  builder: (context, item) => throw UnimplementedError("This does not build nothing"),
                  shouldUse: (context, item) => false,
                ),
            noItemsFoundBuilder,
            errorBuilder,
            width,
            actionsTitle,
            tableActions,
            actionsBuilder,
            ownScroll,
            infiniteScroll,
          );
          // Bolla righe (mobile): stesso container arrotondato, niente bordo.
          boxedSection = Container(
            margin: const EdgeInsets.all(Sizes.gapLg),
            clipBehavior: Clip.antiAlias,
            decoration: BoxDecoration(borderRadius: BorderRadius.circular(Sizes.radiusCard)),
            child: boxedSection,
          );
          if (ownScroll) boxedSection = Expanded(child: boxedSection);

          // Toolbar selezione condivisa tra desktop e mobile (vuota se nessun selectionActionsBuilder).
          final Widget selectionToolbar = selectionActionsBuilder == null
              ? const SizedBox.shrink()
              : Selector<_PagedDataTableState<TKey, TResultId, TResult>, int>(
                  selector: (context, model) => model._rowsSelectionChange,
                  builder: (context, _, __) {
                    final st = context.read<_PagedDataTableState<TKey, TResultId, TResult>>();
                    final selectedCount = st.selectedRows.length;
                    final clTheme = CLTheme.of(context);
                    final tablePrimary = _effectiveTablePrimary(context);

                    Widget toolbarContent;
                    if (selectedCount == 0) {
                      toolbarContent = const SizedBox.shrink(key: ValueKey('toolbar_hidden'));
                    } else {
                      final selectedItems = st.selectedRows.entries.where((e) => e.value < st._items.length).map((e) => st._items[e.value]).toList();
                      final actionWidgets = selectionActionsBuilder!(context, selectedCount, selectedItems);
                      final isDesktop = !_isTableCompact(context);
                      final isAllSelected = st._items.isNotEmpty && st._items.every((it) => st.selectedRows.containsKey(idGetter(it)));
                      toolbarContent = Container(
                        key: const ValueKey('toolbar_visible'),
                        padding: const EdgeInsets.symmetric(horizontal: Sizes.padding, vertical: 10),
                        decoration: BoxDecoration(
                          color: tablePrimary.withValues(alpha: clTheme.opacitySubtle),
                          border: Border(
                            bottom: BorderSide(color: tablePrimary.withValues(alpha: 0.15), width: 1),
                          ),
                        ),
                        child: Row(
                          children: [
                            if (!isDesktop) ...[
                              Transform.scale(
                                scale: 0.85,
                                child: Checkbox(
                                  value: isAllSelected ? true : null,
                                  tristate: true,
                                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                                  visualDensity: VisualDensity.compact,
                                  activeColor: tablePrimary,
                                  checkColor: Colors.white,
                                  side: BorderSide(color: clTheme.borderColor, width: 1),
                                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
                                  onChanged: (_) {
                                    if (isAllSelected) {
                                      st.clearAllSelections();
                                    } else {
                                      st.selectAllRows();
                                    }
                                  },
                                ),
                              ),
                              const SizedBox(width: Sizes.small),
                            ],
                            Container(
                              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
                              decoration: BoxDecoration(
                                color: tablePrimary.withValues(alpha: 0.12),
                                borderRadius: BorderRadius.circular(6),
                              ),
                              child: Text(
                                '$selectedCount selezionat${selectedCount == 1 ? 'o' : 'i'}',
                                style: clTheme.bodyLabel.copyWith(
                                  color: tablePrimary,
                                  fontWeight: FontWeight.w600,
                                  fontSize: 12,
                                ),
                              ),
                            ),
                            if (actionWidgets.isNotEmpty) ...[
                              const SizedBox(width: Sizes.padding),
                              ...actionWidgets,
                            ],
                            const Spacer(),
                            if (isDesktop)
                              TextButton(
                                onPressed: () => st.clearAllSelections(),
                                style: TextButton.styleFrom(
                                  padding: EdgeInsets.symmetric(horizontal: clTheme.gapMd, vertical: clTheme.gapIconText),
                                  minimumSize: Size.zero,
                                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                                ),
                                child: Text(
                                  'Deseleziona tutto',
                                  style: clTheme.bodyLabel.copyWith(
                                    color: clTheme.secondaryText,
                                    fontSize: 12,
                                  ),
                                ),
                              ),
                          ],
                        ),
                      );
                    }

                    return AnimatedSwitcher(
                      duration: const Duration(milliseconds: 220),
                      switchInCurve: Curves.easeOutCubic,
                      switchOutCurve: Curves.easeInCubic,
                      transitionBuilder: (child, animation) => SizeTransition(
                        sizeFactor: animation,
                        axisAlignment: -1,
                        child: FadeTransition(
                          opacity: animation,
                          child: SlideTransition(
                            position: Tween<Offset>(
                              begin: const Offset(0, -0.3),
                              end: Offset.zero,
                            ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)),
                            child: child,
                          ),
                        ),
                      ),
                      child: toolbarContent,
                    );
                  },
                );
          return !_isTableCompact(context)
              ? Stack(
                  children: [
                    Container(
                      decoration: BoxDecoration(
                        color: CLTheme.of(context).secondaryBackground,
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          /* FILTER TAB */
                          if (localTheme.configuration.filterBarVisibile &&
                              (header != null ||
                                  mainMenus.isNotEmpty ||
                                  extraMenus.isNotEmpty ||
                                  selectionActionsBuilder != null ||
                                  state.filters.isNotEmpty)) ...[
                            _PagedDataTableFilterTab<TKey, TResultId, TResult>(
                              mainMenus,
                              extraMenus,
                              header,
                              rowsSelectable,
                              idGetter,
                              downloadPage,
                              downloadButtonText,
                              downloadButtonIcon,
                              isFilterBarRounded,
                              hoistFilterBarToShell,
                              selectionActionsBuilder,
                            ),
                          ],

                          // Desktop: le azioni bulk NON vivono più nella toolbar
                          // (filter tab) — vivono nella card flottante sopra il
                          // footer (vedi _DesktopBulkActionsFloatingCard sotto).

                          /* HEADER ROW — inset Lg (allineato alla bolla). Top Lg: + il
                             centering del testo nella riga (44px) ≈ 2Xl visivo dalla toolbar. */
                          Padding(
                            padding: const EdgeInsets.fromLTRB(Sizes.gapLg, Sizes.gapLg, Sizes.gapLg, 0),
                            child: _PagedDataTableHeaderRow<TKey, TResultId, TResult>(
                                rowsSelectable, width, idGetter, hasAnyActions, hasExpandIcon, actionsColumnWidth, selectAllInHeader),
                          ),
                          /* ITEMS */
                          rowsSection,
                        ],
                      ),
                    ),
                    // Card flottante azioni bulk (desktop): overlay sopra le righe,
                    // ancorata gapLg SOPRA il footer (= bordo inferiore di questa
                    // sezione righe, il footer è la sezione sorella subito sotto).
                    // Non sposta le righe (è in overlay nello Stack).
                    if (selectionActionsBuilder != null)
                      Positioned(
                        left: 0,
                        right: 0,
                        bottom: Sizes.gapLg,
                        child: _DesktopBulkActionsFloatingCard<TKey, TResultId, TResult>(
                          selectionActionsBuilder: selectionActionsBuilder!,
                          idGetter: idGetter,
                        ),
                      ),
                  ],
                )
              : Column(
                  children: [
                    // Filter bar mobile. Il tab DEVE essere costruito anche quando
                    // hoisted (è lui a pubblicare ricerca/filtri/sort nello shell),
                    // ma in quel caso NON deve occupare spazio inline: host → shrink,
                    // niente Container/padding → niente spazio morto in cima.
                    if (localTheme.configuration.filterBarVisibile &&
                        (header != null || mainMenus.isNotEmpty || extraMenus.isNotEmpty || state.filters.isNotEmpty))
                      Builder(builder: (context) {
                        final tab = _PagedDataTableFilterTab<TKey, TResultId, TResult>(
                          mainMenus,
                          extraMenus,
                          header,
                          rowsSelectable,
                          idGetter,
                          downloadPage,
                          downloadButtonText,
                          downloadButtonIcon,
                          isFilterBarRounded,
                          hoistFilterBarToShell,
                          // Hoisted: il tab passa il builder all'host che pubblica
                          // la barra bulk nel bottom shell (selectionBar). Il tab
                          // stesso non la renderizza inline su mobile (isDesktopBar).
                          selectionActionsBuilder,
                        );
                        if (hoistFilterBarToShell) return tab;
                        // Niente SizedBox sotto: il gap verso il container row lo dà
                        // già il suo margine top Lg → solo Lg attorno al container.
                        return Container(
                          width: double.infinity,
                          decoration: BoxDecoration(
                            color: CLTheme.of(context).secondaryBackground,
                            borderRadius: BorderRadius.all(Radius.circular(Sizes.radiusCard)),
                          ),
                          child: Padding(
                            padding: const EdgeInsets.all(Sizes.gapLg),
                            child: tab,
                          ),
                        );
                      }),
                    // Hoisted: la toolbar selezione vive nel bottom shell
                    // (selectionBar) → niente toolbar inline (evita doppione).
                    if (!hoistFilterBarToShell) selectionToolbar,
                    boxedSection,
                  ],
                );
        },
      );
      assert(effectiveTheme.rowColors != null ? effectiveTheme.rowColors!.length == 2 : true, "rowColors must contain exactly two colors");

      final titleHeader = _buildTitleHeader(context);

      // Footer paginazione, condiviso tra le due modalità di layout.
      // Stessa superficie della tabella, separato da hairline superiore.
      final bool footerShown = localTheme.configuration.footer.footerVisible && showFooter && !infiniteScroll;
      final Widget footerSection = footerShown
          // Border top fornito dal solo _PagedDataTableFooter: il wrapper non lo
          // ridisegna, altrimenti doppio hairline sopra la paginazione.
          ? SizedBox(
              width: double.infinity,
              child: _PagedDataTableFooter<TKey, TResultId, TResult>(themeData: localTheme),
            )
          : const SizedBox.shrink();

      return _CLTableStyleScope(
        style: style,
        child: PagedDataTableTheme(
          data: effectiveTheme,
          child: Container(
            // Foundation: card L1 = secondaryBackground + ombra soft (cardShadowSoft),
            // border opt-in (default off). `embedded` → niente card propria: la
            // superficie la fornisce un CLContainer esterno.
            decoration: embedded
                ? null
                : BoxDecoration(
                    color: CLTheme.of(context).secondaryBackground,
                    borderRadius: BorderRadius.circular(Sizes.radiusCard),
                    boxShadow: CLTheme.of(context).cardShadowSoft,
                  ),
            child: Material(
              type: MaterialType.transparency,
              shape: RoundedRectangleBorder(
                // Hairline solo se esplicitamente richiesto (showBorder). Niente
                // bordo auto in dark: la card si appoggia al contrasto bg/righe.
                side: (!embedded && showBorder)
                    ? BorderSide(color: CLTheme.of(context).borderColor, width: 1)
                    : BorderSide.none,
                borderRadius: BorderRadius.circular(embedded ? 0 : Sizes.radiusCard),
              ),
              clipBehavior: Clip.antiAlias,
              child: !_isTableCompact(context)
                  // Solo fillHeight: niente scroll esterno, scorrono solo le righe.
                  // (infiniteScroll senza fillHeight → la pagina scrolla la tabella).
                  ? fillHeight
                      ? Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: [
                            if (titleHeader != null) titleHeader,
                            Expanded(
                              child: Container(
                                decoration: BoxDecoration(
                                  color: CLTheme.of(context).secondaryBackground,
                                ),
                                child: child,
                              ),
                            ),
                            footerSection,
                          ],
                        )
                      : SingleChildScrollView(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.stretch,
                            children: [
                              if (titleHeader != null) titleHeader,
                              Container(
                                decoration: BoxDecoration(
                                  color: CLTheme.of(context).secondaryBackground,
                                ),
                                child: child,
                              ),
                              footerSection,
                            ],
                          ),
                        )
                  : fillHeight
                      ? Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: [
                            if (titleHeader != null) titleHeader,
                            Expanded(child: child),
                            // Padding solo se il footer è mostrato: senza footer
                            // (infinite scroll) niente spazio morto in fondo.
                            if (footerShown) ...[
                              const SizedBox(height: Sizes.padding),
                              // Sotto: solo il bottom padding Lg del footer (no extra).
                              footerSection,
                            ],
                          ],
                        )
                      : SingleChildScrollView(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.stretch,
                            children: [
                              if (titleHeader != null) titleHeader,
                              child,
                              // Padding/footer solo se il footer è mostrato: in
                              // infinite scroll (footer nascosto) niente spazio
                              // morto sotto il messaggio di fine lista.
                              if (footerShown) ...[
                                const SizedBox(height: Sizes.padding),
                                // Sotto: solo il bottom padding Lg del footer (no extra).
                                footerSection,
                              ],
                            ],
                          ),
                        ),
            ),
          ),
        ),
      );
    },
  );

  // Scroll guidato dalla pagina (infiniteScroll senza fillHeight): la tabella
  // gestisce internamente auto-fill + load a fine scroll col controller della
  // pagina. Niente plumbing nei consumer.
  if (infiniteScroll && !fillHeight && pageScrollController != null && controller != null) {
    return _PageScrollAutoFill<TKey, TResultId, TResult>(
      scrollController: pageScrollController!,
      controller: controller!,
      child: tableTree,
    );
  }
  return tableTree;
}