Skip to content

State Views

CDX UI ships five state-aware widgets that every CDX screen composes from. The goal: stop dropping a raw CircularProgressIndicator (or an ad-hoc empty state) into production screens.

AsyncView

Single declarative wrapper for Future / Stream / direct data sources. Internally backed by raw MobX observables and a reactive Observer, so a stream that emits new values will rebuild in place.

dart
AsyncView<List<Area>>(
  future: api.getAll(),
  contentBuilder: (context, areas) => AreasList(areas: areas),
  // Optional overrides — defaults to LoadingState / EmptyState / ErrorState
  loadingBuilder: (_) => const LoadingState(message: 'Loading areas…'),
  emptyBuilder: (_) => const EmptyState(title: 'No areas', subtitle: 'Add your first area to get started.'),
  errorBuilder: (context, error, retry) => ErrorState(error: error, onRetry: retry),
  isEmpty: (areas) => areas.isEmpty,             // optional override
  onRetry: () => store.reload(),
)

Source modes

dart
AsyncView<T>(future: ...)   // one-shot Future
AsyncView<T>(stream: ...)   // ongoing Stream — re-renders on every emission
AsyncView<T>(data: ...)     // direct value (useful with MobX observables)

Exactly one of future, stream, data must be provided.

Empty detection

By default, AsyncView treats null, empty List, empty Map, empty Set, and empty Iterable as empty. Override via isEmpty for custom shapes.

LoadingState

Centered CircularProgress with optional primary message + subtitle.

dart
LoadingState(message: 'Saving…', subtitle: 'This may take a few seconds.')

EmptyState

Centered icon + title + subtitle + optional action widget.

dart
EmptyState(
  icon: FluentIcons.mail_inbox_24_regular,
  title: 'No notifications',
  subtitle: 'You are all caught up.',
  action: Button.outlined(label: 'Refresh', onPressed: store.refresh),
)

Defaults: mail_inbox_24_regular icon, 'No data' title.

ErrorState

Centered tappable error icon + title + message + optional retry button. Tapping the icon opens a diagnostics dialog with the error type, full message, timestamp, and a copy-to-clipboard stack trace section.

dart
ErrorState(
  error: snapshot.error,
  stackTrace: snapshot.stackTrace,
  errorTimestamp: DateTime.now().toUtc(),
  onRetry: store.refresh,
  title: 'Could not load areas',
  subtitle: 'Try again or contact support if the problem persists.',
)

The diagnostics dialog renders even when onRetry is null — users can still inspect and copy details.

RouteErrorView

Full-page error surface for GoRouter.errorBuilder and shell-level exception handlers. Generic — every parameter is opt-in.

dart
GoRouter(
  errorBuilder: (context, state) => RouteErrorView(
    title: 'Page not found',
    subtitle: state.error?.toString() ?? 'The page you requested does not exist.',
    onHomePressed: () => context.go('/'),
    homeLabel: 'Back to Home',
  ),
  ...
);

In non-release builds, when error is non-null the view also renders a collapsible "Error details" panel with the raw error text.

Field formatting

State views often render formatted values (dates, numbers, durations, entity references). The cdx_field_formatting package ships FieldFormatter.instance — a singleton dispatch registry that gives every CDX surface the same formatted output.

dart
import 'package:cdx_field_formatting/cdx_field_formatting.dart';

// Settings (locale, date pattern, currency, timezone)
FieldFormatter.instance.updateSettings(FormatSettings(
  locale: 'en-US',
  dateFormat: 'MMM d, y',
  timeFormat: 'h:mm a',
));

// Typed convenience
FieldFormatter.instance.formatDate(area.createdAt);
FieldFormatter.instance.formatDateTime(area.updatedAt);
FieldFormatter.instance.formatNumber(123456.789);
FieldFormatter.instance.formatCurrency(42.5, 'USD');
FieldFormatter.instance.formatBytes(1234567);
FieldFormatter.instance.formatDuration(const Duration(minutes: 90));

// Field-name dispatch — dispatches via aliases or regex patterns
FieldFormatter.instance.formatField('created_at', area.createdAt);  // matches /_at$/ → datetime
FieldFormatter.instance.formatField('is_active', area.isActive);    // matches /^is_/ → bool

// Widget version (for JSON / DateTime / icon-value formatters)
FieldFormatter.instance.formatFieldWidget(context, 'created_at', area.createdAt);

Built-in field-name patterns include: _at$ → datetime, _on$ → date, _date$ → date, _time$ → time, _by$ → user_id, _count$ → integer, _percent$ → percent, ^is_ → bool, ^has_ → bool, and more — see Field Formatting for the full package surface.

Every formatter reads FormatSettings reactively (via MobX), so a single updateSettings call live-updates every formatted value in the app inside an Observer.

  • CardsSectionCard.stateful for in-card async states
  • Theming — error / surface tokens
  • LayoutMasterDetailScaffold hosts AsyncView in master + detail