Skip to main content

Provider

Global state needs a way to reach the hooks that consume it without threading values through constructor arguments. utopia_hooks solves this with its own provider mechanism: values are placed in the tree and resolved by type with useProvided<T>(). It is the same idea as an InheritedWidget, but it works from inside a hook and rebuilds the hook when the value changes.

There are three ways to provide a value, for three different situations: a root-level container for app-wide states, and two in-tree wrappers for everything else.

HookProviderContainerWidget

HookProviderContainerWidget registers global states at the app root. It takes an ordered map from each state's type to the hook that builds it, runs every hook, and makes the results available to the whole subtree.

// The ordered dependency list. Each entry is keyed by its own state type.
const _providers = <Type, Object? Function()>{
AuthState: useAuthState,
SettingsState: useSettingsState, // <- May useProvided<AuthState>(): registered above
};

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return const HookProviderContainerWidget(
_providers,
alwaysNotifyDependents: false, // <- Notify dependents only on actual value change
child: MaterialApp(home: Scaffold()),
);
}
}

The map is a dependency list, not an unordered bag: a hook may useProvided only the states registered above it. SettingsState can depend on AuthState because AuthState comes first; reverse them and the lookup throws.

// DON'T - SettingsState depends on AuthState, which is registered below it
const _providers = <Type, Object? Function()>{
SettingsState: useSettingsState, // <- Throws: AuthState not provided yet
AuthState: useAuthState,
};

alwaysNotifyDependents defaults to true, which notifies dependents on every refresh. Production apps usually pass false, so a dependent rebuilds only when the value it reads actually changes (via ==). The BuildContext is registered automatically, so any state hook may call useBuildContext.

This is the registration step from the Global state guide; the ordering rules and the full app-bootstrap recipe live there.

useProvided

A consumer reads a provided value by type. The lookup registers the hook as a dependent, so the hook rebuilds whenever that value changes.

// Any screen or global state hook reads a registered state with one line.
ProfileScreenState useProfileScreenState() {
final auth = useProvided<AuthState>(); // <- Rebuilds this hook when AuthState changes

// Guard on the readiness signal before using the data.
if (!auth.isInitialized) return const ProfileScreenState(canEdit: false);

return ProfileScreenState(canEdit: auth.isLoggedIn);
}

The same call resolves a global state, a value from an in-tree provider, or the BuildContext. Group useProvided calls at the top of the hook so the dependencies read at a glance, mirroring the ordered list at the root.

useProvided reads only utopia_hooks' own mechanism - the container and the two wrappers below. It does not read package:provider or other InheritedWidget containers. To consume those, retrieve the BuildContext and read them through it. See the useProvided reference for the full signature, the useProvidedUnsafe runtime-type variant, and the missing-value behavior.

ValueProvider

For a value that needs no hook - a constant, configuration, or an object a parent already computed - wrap the subtree in ValueProvider. It provides a single value, keyed by its static type.

Widget buildWithConfig(AppConfig config, Widget child) {
// Provide an already-computed value to the subtree, keyed by its type.
return ValueProvider(config, child: child);
}

// Read it anywhere below with the same lookup used for global states.
AppConfig useApiBaseConfig() => useProvided<AppConfig>();

Because it keys the value by its static type T, providing a subtype through a supertype variable provides it under the wrong key:

// CAREFUL
final Object config = AppConfig();
ValueProvider(config, child: child); // <- Provided as Object, not AppConfig

Use ValueProvider for static or already-computed values, and the root container plus a hook for reactive app-wide state.

HookProvider

HookProvider runs a hook and provides its result to the subtree, re-providing a fresh value whenever the hook rebuilds. It scopes a piece of hook-driven state to one part of the tree, rather than registering it globally at the root.

class CartState {
final int itemCount;

const CartState({required this.itemCount});
}

CartState useCartState() {
final itemCountState = useState(0);

return CartState(itemCount: itemCountState.value);
}

Widget buildShop(Widget child) {
// Scope a piece of hook-driven state to one part of the tree, rather than
// registering it at the root. The value re-provides as the hook rebuilds.
return HookProvider(useCartState, child: child);
}

HookProvider is a HookWidget, so the function runs in a real hook context and may call other hooks. Reach for it when a state is hook-driven but only relevant to a section of the app - a cart inside the shop flow, a wizard's state inside the wizard.

tip

For providing several values at once under custom keys there is the lower-level ProviderWidget that both wrappers are built on. In app code, prefer ValueProvider and HookProvider.

Choosing between them

SituationUse
Reactive state shared app-wide (auth, settings)HookProviderContainerWidget + hook, at the root
Already-computed value handed down (config, a constant)ValueProvider
Hook-driven state scoped to part of the treeHookProvider
Several values under custom keysProviderWidget

See also