drip 0.1.1
drip: ^0.1.1 copied to clipboard
Minimalist Cubit-style state management for Flutter using Streams and InheritedWidget.
Drip #
Minimalist Cubit-style state management for Flutter — built on Stream + InheritedWidget. Drip is small on purpose: ~50 lines of core, three composable widgets, and a multi-provider out of the box.
Why drip? #
- Tiny mental model. Subclass
Drip<S>and callleak(newState)to emit. That's it. - No code generation, no boilerplate. Plain Dart classes; no events, no reducers, no Freezed required.
- Stream-based. Every
Dripexposes a broadcaststateStreamyou can also consume outside the widget tree. - Multi-provider built in. Compose several
Drips throughMultiProvider(powered bypackage:nested). - Selector with memoization.
DropWidgetrebuilds only when the value picked by your selector changes — supportsList,Map, and primitive equality. - Composable. A
ComputedDripderives its state from one or more source drips. Edit auserdrip and agreetingdrip updates automatically — without a single line of glue. Computed drips can be sources for further computed drips. - Async-aware.
AsyncDrip<T>ships a sealedidle / loading / data / errorstate with arun(Future<T>)helper. Switch over it with Dart 3 pattern matching — no third-partyAsyncValuepackage, no glue code.
| drip | Cubit (bloc) | Riverpod | |
|---|---|---|---|
| Published archive | ~78 KB | ~250 KB+ | ~300 KB+ |
| Events / reducers | no | yes (events optional in Cubit) | no |
| Selector widget | built-in | BlocSelector |
ref.watch(provider.select) |
| Computed / derived state | built-in (ComputedDrip) |
manual (re-emit by hand) | Provider derivation |
| Async state (loading/data/error) | built-in (AsyncDrip<T> + sealed states) |
manual states per Cubit | AsyncValue<T> |
| Code gen needed | never | optional | optional |
| Learning surface | one base class + two specialisations + four widgets | many | many |
Drip is intentionally a small library, not a framework. If you need devtools, persistence, async value handling, or sophisticated dependency injection, reach for Riverpod or Bloc. If you want the simplest "state holder + stream" possible, Drip fits.
Install #
dependencies:
drip: ^0.1.0
import 'package:drip/drip.dart';
Quickstart #
1. Define your state holder #
class CounterDrip extends Drip<int> {
CounterDrip() : super(0);
void increment() => leak(state + 1);
void reset() => leak(0);
}
2. Provide it to the widget tree #
DripProvider<CounterDrip>(
create: (_) => CounterDrip(),
child: const CounterPage(),
)
3. Consume it #
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Dripper<CounterDrip, int>(
builder: (drip, state) => Text('Counter: $state'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterDrip>().increment(),
child: const Icon(Icons.add),
),
);
}
}
The three consumer widgets #
| Widget | Purpose | Rebuilds? |
|---|---|---|
Dripper<D, S> |
Builder + optional listener. Like BlocConsumer. |
On every new state |
Dripping<D, S> |
Listener only (side effects). | Never — child is rebuilt by its parent |
DropWidget<D, S, T> |
Selector with memoization. | Only when selector(state) returns a new value |
// Selector — rebuild only when the .count field changes,
// even if other fields of CounterState are mutated.
DropWidget<CounterDrip, CounterState, int>(
selector: (state) => state.count,
builder: (drip, count) => Text('$count'),
)
Accessing a Drip from BuildContext #
final drip = context.read<CounterDrip>(); // no listening
final drip = context.watch<CounterDrip>(); // listen to updates
final drip = Dropper.of<CounterDrip>(context);
Multi-provider #
When several drips need to be available in the same subtree:
MultiProvider(
children: [
Dropper<UserDrip>(drip: UserDrip()),
Dropper<CartDrip>(drip: CartDrip()),
Dropper<ThemeDrip>(drip: ThemeDrip()),
],
child: const MyApp(),
)
MultiProvider is a thin alias over Nested from package:nested, so the children compose without indentation hell.
Composing drips (computed state) #
Sometimes one piece of state is a function of others. Instead of hand-wiring stream.listen → leak, declare a ComputedDrip:
class UserDrip extends Drip<User> {
UserDrip() : super(User.empty());
void setName(String n) => leak(state.copyWith(name: n));
}
final user = UserDrip();
// Inline form
final greeting = ComputedDrip<String>(
initial: '',
sources: [user],
compute: () => 'Hello ${user.state.name}',
);
// Subclass form
class GreetingDrip extends ComputedDrip<String> {
GreetingDrip(this.user) : super(
initial: '',
sources: [user],
compute: () => 'Hello ${user.state.name}',
);
final UserDrip user;
}
A ComputedDrip is itself a Drip, so it can be a source for further computed drips — composition is transitive. It only re-emits when the new value differs from the current one (by ==), and it cancels its source subscriptions on close().
Async state with sealed pattern matching #
Most apps fetch things. Instead of inventing a third loading/error mechanism per feature, extend AsyncDrip<T> and let it sequence Loading -> Data | Error for you:
class UserDrip extends AsyncDrip<User> {
UserDrip(this._api);
final UserApi _api;
Future<void> load(int id) => run(() => _api.fetchUser(id));
}
In the view, exhaustive switch on the four cases (Dart 3 pattern matching):
Dripper<UserDrip, AsyncState<User>>(
builder: (_, state) => switch (state) {
AsyncIdle() => const Text('Tap to load'),
AsyncLoading(:final previous) => previous == null
? const CircularProgressIndicator()
: Text('Refreshing: ${previous.name}'), // stale-while-revalidate
AsyncData(:final value) => Text(value.name),
AsyncError(:final error) => Text('Error: $error'),
},
);
AsyncLoading.previous carries the last successful value so the UI can keep showing data while refreshing — a "stale-while-revalidate" pattern in three keystrokes. Use state.dataOrNull if you only ever care about the data, regardless of case.
Observability #
For logging, analytics, or debugging, install a DripObserver at startup. It's a single global hook with no-op defaults — override only what you need:
class LoggingObserver extends DripObserver {
@override
void onChange(Drip<Object?> drip, Object? previous, Object? next) {
debugPrint('${drip.runtimeType}: $previous -> $next');
}
}
void main() {
Drip.observer = LoggingObserver();
runApp(const MyApp());
}
Available hooks: onCreate, onChange(previous, next), onClose.
Mental model #
┌──────────────────────────────────────────────┐
│ Drip<S> │
│ _state: S leak(newState) │
│ _controller: StreamController<S>.broadcast │
└──────────────────────────────────────────────┘
│ stateStream
▼
┌─────────────────────────────────────────────┐
│ Dropper<D> (InheritedWidget provider) │
└─────────────────────────────────────────────┘
│ │ │
Dripper Dripping DropWidget
(rebuild on (side effect, (rebuild on
new state) no rebuild) selector change)
Examples #
- Counter — minimal
Drip<S>withDripperandDropWidget - Todo — list management, multiple actions, custom state class
What's next #
The library is intentionally small. These features are being considered for the next minor releases — open an issue with a +1 to vote:
DripFamily<K, D>— parametrized factories.userFamily(userId)returns the sameDripfor the same key, with explicitdispose(key)/disposeAll(). Fills the Riverpodfamilyniche in one class.RewindableDrip<S>— circular history buffer withrewind()/forward()and boundedhistorySize. Brings back theMemoryInterceptoridea from0.0.1as a first-class subclass.PersistedDrip<S>— opt-in hydration via a pluggableDripStorageadapter (works withshared_preferences,hive, in-memory, or your own). Hydrates state on construction and persists on everyleak.- DripDevTools extension — inspect the active drips, their current state, and the observer chain from Flutter DevTools. Built on top of
DripObserver, no app instrumentation required. - Generated
MultiProviderbuilders — codegen sugar to declare a list of drips and get the wired-up provider tree, while keeping codegen strictly optional. Drip.dispatch((state) => newState)— single-call mutation helper to skip the typicalvoid increment() => leak(state + 1)boilerplate for one-liners.
If you want one of these now or have ideas for others, please open an issue.
Maintainers #
Contributions welcome.
License #
MIT — see LICENSE.