Published 2025-09-03
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'),
),
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.