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.
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
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.
LoadingState(message: 'Saving…', subtitle: 'This may take a few seconds.')EmptyState
Centered icon + title + subtitle + optional action widget.
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.
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.
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.
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.