Published 2026-09-22
Scoped, widget-based state management inspired by provider.

For ChangeNotifier based data models that can trigger updates for your UI, or just plain objects for dependency injection.
More or less backwards compatible with the provider package. While it is (still) possible to use Provider and ChangeNotifierProvider from that package, it is not recommended to do so.
If you want to provide static resources like (singleton) service clients, settings or otherwise, use a service locator like GetIt.

Models must all extend or implement ChangeNotifier.
class Model extends ChangeNotifier {
late Timer _timer;
late DateTime _dateTime;
Model() : super() {
_timer = Timer.periodic(const Duration(seconds: 1), _ON_Timer);
}
DateTime get datetime => _dateTime;
@override
void dispose() {
_timer.cancel();
super.dispose();
}
void _ON_Timer(final Timer timer) {
_dateTime = DateTime.now();
notifyListeners();
}
}
MyProvider<Model>(
create: (final _) => Model(),
child: ...,
),
Children will not update when the model triggers if they are not wrapped with a consumer.
MyProvider<Model>(
create: (final _) => Model(),
child: Text('Will not update: ${DateTime.now().toIso8601String()}'),
),
Note that when the provider is disposed, the model is also disposed (if the model is a ChangeNotifier), (unless you set shouldDispose: false) so if you construct your provider from a static resource like create: (final _) => GetIt.instance.get<Model>(), the resource is not valid anymore. Should not be a problem if the dispose happens when the application exits.
MyProvider<Model>(
create: (final _) => GetIt.instance.get<Model>(),
shouldDispose: false,
child: Text('Will not update: ${DateTime.now().toIso8601String()}'),
),
Providing a model through an interface will also work:
MyProvider<ISomeModelInterface>(
create: (final _) => SomeModelImpl(),
child: ...
),
Use MyMultiProvider to provide multiple providers. The providers must not have a child widget, while the multi-provider must have one.
MyMultiProvider(
providers: <SingleChildWidget>[
MyProvider<Model>(
create: (final _) => Model(),
),
MyProvider<OtherModel>(
create: (final _) => OtherModel(),
),
MyProvider<ISomeModelInterface>(
create: (final _) => SomeModelImpl(),
),
...
],
child: Wrap(
children: <Widget>[
Text('Will not update: ${DateTime.now().toIso8601String()}'),
],
),
),
Wrap your widget in a MyConsumer regardless of what model properties are changed:
MyConsumer<Model>(
builder: (context, model, child) =>
Text(DateTime.now().toIso8601String()),
),
MyConsumer<Model>(
shouldUpdate: false,
builder: (context, model, child) =>
Text(DateTime.now().toIso8601String()),
),
Wrap your widget in a MySelector regardless of what model properties are changed:
MySelector<Model, void>(
builder: (context, model, select, child) =>
Text(DateTime.now().toIso8601String()),
),
When a specific property changes:
MySelector<Model, DateTime>(
select: (current) => current.datetime,
builder: (context, model, select, child) =>
Text(select.toIso8601String()),
),
When a condition is applied to a selected property:
MySelector<Model, DateTime>(
select: (current) => current.datetime,
shouldUpdate: (previous, current) =>
current.second.isEven,
builder: (context, model, select, child) =>
Text(select.toIso8601String()),
),
When a select returns a record, it is advised to implement the shouldUpdate callback, because the select will always return a new object and thus the selector always triggers:
MySelector<Model, ({bool busy, bool done})>(
select: (current) => (busy: current.busy, done: current.done),
shouldUpdate: (previous, current) =>
previous.busy != current.busy || previous.done != current.done,
builder: (context, model, select, child) =>
model.done ? Text('done') : Text(select.busy ? 'busy' : 'idle'),
),
ConsumerBuilder1/2/3/4 and SelectorBuilder1/2/3/4 typedef return types from Widget back to dynamic, restoring support for non-Widget return values (e.g., DataRow for DataTable). Returning a non-renderable object (including null) compiles silently but crashes at runtime.HHSelector (and variants 2–4) shouldUpdate predicate signature from bool Function(S? previous, S current)? to bool Function(S previous, S current)? — previous is no longer nullable; it is always initialised before the first _ON_Change fires.HHProvider: added optional onCreated: void Function(T)? callback, invoked immediately after the model is created.HHProvider: added optional onDisposed: void Function()? callback, invoked immediately after the model is disposed.HHSelector (and variants 2–4): builder parameter is now optional (nullable); the widget renders SizedBox.shrink() when builder is null, enabling use with onUpdate-only patterns.AGENTS.md: updated Builder Type Safety convention to reflect the dynamic typedef; updated Selector correctness description to document the eager _previous update in _ON_Change and the capturedPrev capture.SKILL.md: added Installation section, isDisposed/isNotDisposed property table, onCreated/onDisposed callback example, HHProvider.of<T>()/maybeOf<T>() static method reference table, and a Gotchas and Common Mistakes section. Fixed stray embedded line numbers in Step 5.HHSelector (and variants 2–4): _disposeUpdateHandler now removes _ON_Change unconditionally regardless of widget.shouldUpdate, closing the dangling-listener window when a parent rebuilds with shouldUpdate: false after it was true.HHSelector, HHSelector3, and HHSelector4 ("renders SizedBox.shrink when builder is null").hh_selector_rapid_changes_test.dart with additional rapid-change transition scenarios for all selector variants.hh_selector_3_4_test.dart with null-builder and shouldUpdate coverage for HHSelector3 and HHSelector4.hh_selector_test.dart with non-nullable shouldUpdate predicate cases.ConsumerBuilder and SelectorBuilder typedefs: absorbed the loose inline comment into each typedef's doc, corrected "Must return a Widget" to accurately reflect the generic R return type, and added type parameter descriptions ([R], [T], [T1]/[T2]/…, [S]).HHSelector variants (1–4): _updateNotifier now captures the selected value at the moment _ON_Change fires (capturedSelect) instead of reading _select from the instance field inside the deferred closure. Previously, two rapid consecutive model changes before the microtask queue drained caused onUpdate to report (A, C) for the first notification instead of (A, B).context.watch<T>(): now mirrors HHProvider.of by checking widget presence via getInheritedWidgetOfExactType before throwing, so providers whose value is null are not incorrectly treated as absent when T is a nullable type.HHConsumer variants (1–4): _disposeUpdateHandler now removes _ON_Change unconditionally, regardless of widget.shouldUpdate. Previously a runtime change of shouldUpdate from true to false could leave a dangling listener attached to the model.HHSelector variants (1–4): added _initialized bool flag so debugFillProperties no longer accesses _select or _previous before didChangeDependencies has initialised them, preventing LateInitializationError in the Flutter Widget Inspector.ObjectNotChangeNotifierException constructor doc: was incorrectly labelled "ProviderNotFound".ConsumerBuilder and SelectorBuilder typedefs from dynamic to Widget return type, restoring compile-time type safety for builder callbacks.futureWrapper doc comment with an execution-timing diagram, design rationale comparing microtasks to addPostFrameCallback, and an explanation of the async while drainer.pkg_state_management.dart covering all key classes, context extensions, and utilities.ConsumerBuilder/SelectorBuilder typedefs, HHValueListenableBuilder, HHValueNotifier, Selector<T, S>, and HHValueSelectorBuilder class._HHOptionalConsumerState.AGENTS.md and SKILL.md entries for Selector<T, S> typedef, corrected HHValueSelectorBuilder widget type (was StatelessWidget, is StatefulWidget), documented nullable-T watch correctness, capturedSelect invariant, and unconditional listener cleanup.hh_consumer_3_4_test.dart, hh_selector_3_4_test.dart, hh_selector_rapid_changes_test.dart (regression tests for the capturedSelect fix across all selector variants), hh_value_selector_builder_3_4_test.dart, read_context_nullable_test.dart (regression tests for the nullable-T watch fix). Total test count: 78 → 135.HHProvider.maybeOf lookup performance (O(1) common case) using manual stack trace scanning.HHChangeNotifier stack trace parsing for debug logging using high-performance string scanning.HHSelector variants (1-4) by caching selection results immediately during notification to eliminate redundant re-evaluations during microtask build phase.HHConsumer and HHSelector variants (1-4) to use context.watch<T>() and instance comparison in didChangeDependencies for reactive model swap support.updateShouldNotify in _HHProvider to trigger dependent updates only when the model instance identity changes.HHValuesNotifier implementation to follow IDisposable pattern and documented lifecycle safety to prevent memory leaks.context.read and context.maybeRead from initState after setting the allow flag to true.didChangeDependencies initializers to initState.ConsumerBuilder and SelectorBuilder typedefs from dynamic return type to Widget.context.read<T>() to use HHProvider.of<T>() for proper nullable type handling.context.watch<T>() to use dependOnInheritedWidgetOfExactType directly.HHOptionalConsumer to check for inherited widget presence instead of using maybeRead.HHProvider.of<T>() to check widget presence rather than value nullability, fixing false "not found" errors for nullable types.HHProvider to call onDisposed callback after successful disposal (was before).HHProvider.maybeOf to optimize stack trace inspection by joining only top 10 lines once.HHValueSelectorBuilder from StatelessWidget to StatefulWidget to properly track selected values and only rebuild when selection changes.HHValuesNotifier to use internal ValueNotifier for listener management and proper disposal.HHChangeNotifier.dispose() to call super.dispose() only after clearing debouncers and marking as disposed.HHChangeNotifier.notifyListeners() to check _disposed before calling super.notifyListeners().HHChangeNotifier._contextOf() to guard against out-of-bounds access when stack trace is too short.HHConsumer variants (1-4) to capture current model(s) at scheduling time to prevent stale callbacks after provider swap.HHSelector variants (1-4) to simplify value incrementing logic and re-evaluate selection inside microtask to guard against model bouncing.HHSelector variants (1-4) to update _previous after notification instead of before.futureWrapper to execute each callback inside try/catch to prevent one failure from blocking remaining callbacks.clearPendingMicrotasks() helper function for test cleanup.HHChangeNotifier, HHProvider, HHConsumer, HHSelector, HHValueSelectorBuilder, HHValuesNotifier, futureWrapper).pkg_core_flutter for the lints file only that caused a circular dependency.HHValueNotifier (alias for ValueNotifier).HHValueSelectorBuilder for selective building.HHValueListenableBuilder (2, 3, 4) (alias for ValueListenableBuilder).isWeb and isDebugMode lookups.maybeWatch and watch (like maybeRead and read, but with update hooks).maybeOf:context.dependOnInheritedWidgetOfExactType<_HHProvider<T>>()?.value; to maybeOf:context.getInheritedWidgetOfExactType<_HHProvider<T>>()?.value; because dependOnInheritedWidgetOfExactType registers a dependency on a particular type by calling this method, and getInheritedWidgetOfExactType does not (our consumers handle everything with listeners, not implicit updates).HHChangeNotifier to show notifyListeners calls in debug mode.builder of HHConsumer optional, renders const SizedBox() if not given. This way you can handle changes in the onChange callback only, while not rendering any widget.HHValuesNotifier, internal val now private.if value != null ? something() : otherwise() to something?.call() ?? otherwise()._previous after the notifier trigger and onChange, otherwise previous and current would always be the equal since the trigger is within a Future.microtask call.HHValuesNotifier.notifyListeners is called from within a setState call.material dependency.Future<void>.microtask in consumers and selectors.HHOptionalConsumer that renders its child only if the requested provider is available.Future<void>.microtask for consumers and selectors.HHSelector with records in README.md.notifyListenersDebounced from HHChangeNotifier when disposed.HHChangeNotifier, super.notifyListeners in a microtask.Provider.didChangeDependencies from Provider.initState so a provider constructor can lookup other providers by using context.read<YourProvider>() because that was not possible from initState.SingleChildWidget export in separate library file.BuildContext _ parameters to BuildContext context.HHChangeNotifier::notifyListenersDebounced.HHChangeNotifier that blocks notifications when the model is disposed.pkg_core interface for IDisposable.context from a Builder.HHProvider without Builder in buildWithChild.onChange callback to notifier function, if debounced gets triggered once.provider dependency.didChangeDependencies was triggered.select property of HHSelector is now mandatory.dynamic internals are now typed S.HHProvider.of and HHProvider.maybeOf to allow calling from suspicious contexts.Future.microtask.ints.onUpdate callbacks to consumers.My to HH tokens in class names.dynamic (not Widget), in case your builder must return for example a DataRow for a PlutoGrid.MyChangeNotifier.MySelector4 and MyConsumer4.onUpdate callback on MyConsumer.MyConsumer into separate MySelector for conditional updating.shouldUpdate, previous on the first try should be null, otherwise we're missing the initial update.ChangeNotifier anymore.README.md.doc from .pubignore.README.md link to screenshot.