holders

Quality

Lightweight reactive values for Flutter. A holder owns or adapts a value, notifies callbacks when it changes, and can rebuild widgets without requiring a larger state-management framework.

Features

  • Store values with ValueHolder.
  • Derive and update values through LinkHolder.
  • Adapt ValueNotifier instances with NotifierHolder.
  • Adapt callback-based state with GetterHolder.
  • Ignore equivalent values with a custom comparator.
  • Debounce notifications with stress and throttle them with cooldown.
  • Rebuild widgets with HolderBuilder or HoldersBuilder.

Installation

Add holders to your pubspec.yaml:

dependencies:
  holders: ^0.1.0

Then import it:

import 'package:holders/holders.dart';

Usage

Create a holder and subscribe to changes:

final counter = ValueHolder<int>(
  0,
  isEqual: nativeIsEqual,
);

void logValue(int value) => print('Counter: $value');

counter.hold(logValue);
counter.value = 1; // Prints: Counter: 1
counter.value = 1; // Ignored because the value is equal.
counter.unhold(logValue);

Create a two-way derived value with link:

final temperature = ValueHolder<double>(20);
final fahrenheit = temperature.link<double>(
  read: (celsius) => celsius * 9 / 5 + 32,
  write: (fahrenheit) => (fahrenheit - 32) * 5 / 9,
  isEqual: nativeIsEqual,
);

fahrenheit.value = 86;
print(temperature.value); // 30.0

Rebuild a widget when a holder changes:

HolderBuilder(
  holder: counter,
  builder: (context) => Text('${counter.value}'),
)

Create a Flutter listenable when an API requires one, and dispose it when its owner is disposed:

final listenable = counter.buildListener();

// Use listenable with a ValueListenableBuilder.

listenable.dispose();

Holder values update immediately. stress waits until changes stop before notifying callbacks, emitting only the latest value. cooldown emits the first notification immediately and then, at most once per interval, emits the latest value received during that interval. When both are set, stress runs first and cooldown starts after the changes settle:

final query = ValueHolder<String>(
  '',
  isEqual: nativeIsEqual,
  stress: const Duration(milliseconds: 300),
);

Notifications can be disabled until a particular value is observed or a condition is met. These methods also support nullable values:

final selection = ValueHolder<int?>(0);

selection.turnOffUntilValue(null);
selection.value = null; // Turns on and emits null.

selection.turnOffUntil(
  (value) => value != null && value.isEven,
  excludeMatchingCall: true,
);
selection.value = 2; // Turns on without emitting 2.

selection.dispose();

Lifecycle

Call dispose() when a holder is no longer needed. This cancels its timers, removes callbacks, and disconnects linked or notifier-backed holders from their source. Disposal is permanent: attempts to access the value or reactivate the holder throw StateError. Status and configuration reads, removing a callback with unhold(), and calling dispose() again remain safe.

Linked holders and read-only listeners are disposed automatically with their source. NotifierHolder does not own the supplied ValueNotifier; dispose that notifier separately when appropriate.

Listenables and notifiers created with buildListener() or buildNotifier() are owned by their caller and must be disposed. Disposing the holder disconnects them but does not dispose them.

Libraries

holders
Lightweight reactive values, adapters, and builder widgets for Flutter.