Skip to main content

ComputedStateWrapper

Renders one of three builders for a ComputedState: a loading builder while the value is computing, a failure builder when it failed, and the main builder once a value is ready. It is the widget counterpart of useComputedState / useAutoComputedState - drop it into a tree to branch on a computed state without pattern-matching the sum type by hand.

class ArticleView extends HookWidget {
const ArticleView({super.key, required this.service, required this.id});

final ArticleService service;
final String id;


Widget build(BuildContext context) {
final articleState = useAutoComputedState(() => service.load(id), keys: [id]);

return ComputedStateWrapper<Article>(
state: articleState,
inProgressBuilder: (context) => const Center(child: CircularProgressIndicator()),
failedBuilder: (context) => const Center(child: Text('Could not load the article')),
builder: (context, article) => Text(article.title), // <- only runs once value is ready
);
}
}

Constructor

ComputedStateWrapper<E>({
Key? key,
required ComputedState<E> state,
required Widget Function(BuildContext) inProgressBuilder,
required Widget Function(BuildContext) failedBuilder,
required Widget Function(BuildContext, E) builder,
bool keepInProgress = false,
});
  • state - any ComputedState<E>, including the MutableComputedState returned by useComputedState / useAutoComputedState.
  • inProgressBuilder - shown while state.value is notInitialized or inProgress and no value is available yet.
  • failedBuilder - shown when state.value is failed.
  • builder - shown once a value is ready; receives the resolved E (never null).
  • keepInProgress (false by default) - when true, the last ready value stays on screen while a new computation runs, instead of falling back to inProgressBuilder.

The loading and failure builders are wrapped so they fill a scroll view if one is the parent - they render correctly under a RefreshIndicator or sliver without extra Center/SingleChildScrollView boilerplate.

Use cases

  • Rendering a single async value - a detail screen, a card backed by useAutoComputedState - where you want explicit loading and error UI without writing a HookWidget and a state.value.when(...) by hand.

  • Keeping the previous value visible across a reload. With keepInProgress: true, switching the input (a new id in keys) keeps the current content on screen while the next value loads, avoiding a spinner flash:

    class KeepInProgressView extends HookWidget {
    const KeepInProgressView({super.key, required this.service, required this.id});

    final ArticleService service;
    final String id;


    Widget build(BuildContext context) {
    final articleState = useAutoComputedState(() => service.load(id), keys: [id]);

    return ComputedStateWrapper<Article>(
    state: articleState,
    // With keepInProgress, the previous article stays on screen while the next id loads,
    // instead of flashing the spinner between values.
    keepInProgress: true,
    inProgressBuilder: (context) => const Center(child: CircularProgressIndicator()),
    failedBuilder: (context) => const Center(child: Text('Could not load the article')),
    builder: (context, article) => Text(article.title),
    );
    }
    }

For a value backed by a RefreshableComputedState that should also support pull-to-refresh, use RefreshableComputedStateWrapper instead. For an iterable that needs a distinct empty state, use ComputedIterableWrapper.

Caveats

  • builder never receives null. It only runs once a value is ready (or, with keepInProgress, while the previous value is still held), so there is no nullable case to handle inside it.

  • failedBuilder does not receive the error. It is a Widget Function(BuildContext) with no exception argument, so it can only render a generic failure state. When you need the error object - to show a message or a retry tied to a Retryable - read state.value directly with .when(...) instead of using this wrapper.

  • keepInProgress shows stale content during the reload. The previous value remains visible while the new computation runs, which is the point - but it does mean the UI is briefly out of date. Don't combine it with a separate spinner that implies fresh data.

  • A notInitialized state renders inProgressBuilder, same as inProgress. The wrapper does not distinguish "never started" from "running"; if a state is on-demand and may sit idle, that idle state looks like loading here. Use useAutoComputedState (which starts computing on build) or branch on state.value yourself.

See also