flutter_caffeine 3.0.0
flutter_caffeine: ^3.0.0 copied to clipboard
Flutter bindings for caffeine. Attach reactive scopes to the widget tree, read store state with context.state(), fire events with context.fire() — no StreamBuilders, no dispose boilerplate.
flutter_caffeine #
Flutter bindings for caffeine. Attaches reactive scopes to the widget tree and exposes state reads and event dispatch directly on BuildContext — no StreamBuilders, no dispose overrides, no wrapper widgets.
Table of Contents #
- Installation
- The Caffeine Widget
- Reading State in Widgets
- Firing Events
- Dependency Injection and Testing
Installation #
dependencies:
caffeine: ^3.0.0
flutter_caffeine: ^3.0.0
import 'package:caffeine/caffeine.dart';
import 'package:flutter_caffeine/flutter_caffeine.dart';
The Caffeine Widget #
Caffeine creates a caffeine Scope and attaches it to a point in the element tree. The scope is created once (in initState) via the scopeFactory callback and disposed automatically when the element is removed from the tree.
Root Scope Setup #
Place a Caffeine widget near the root of your app to create the root scope:
void main() {
runApp(
Caffeine(
scopeFactory: (_) => Scope(),
child: const MyApp(),
),
);
}
Pass overrides to inject stores or bind global events:
Caffeine(
scopeFactory: (_) => Scope(overrides: {
MappingStoreOverride(from: apiStore, to: productionApiStore),
globalSaveEvent, // bound here — fires broadcast to all descendants
}),
child: const MyApp(),
)
Forking a Child Scope #
Use Caffeine.of(context) inside scopeFactory to fork a child scope. Stores bound to the child live only as long as that Caffeine widget:
// screenStore is disposed when this widget leaves the tree.
Caffeine(
scopeFactory: (context) => Caffeine.of(context).fork(overrides: {
screenStore,
}),
child: const ScreenWidget(),
)
Typical scope structure in a Flutter app:
Caffeine (root) ─── global overrides, app lifetime
│
├── Caffeine ─── homeStore, home screen lifetime
│
└── Caffeine ─── profileStore, profile screen lifetime
│
└── Caffeine ─── editFormStore, edit modal lifetime
Reading State in Widgets #
context.state() — Subscribe and Rebuild #
Call context.state(store) inside build. When listen: true (the default), the widget rebuilds automatically whenever the store's value changes:
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final count = context.state(counterStore);
return Text('$count');
}
}
You can read multiple stores in a single build. Each call subscribes independently:
Widget build(BuildContext context) {
final user = context.state(userStore);
final doubled = context.state(doubledCounterValue);
return Column(children: [
Text(user.name),
Text('$doubled'),
]);
}
Subscriptions are established once per store per element and deduplicated across rebuilds. Cleanup happens via a Finalizer on the element — when it leaves the tree and is garbage collected, subscriptions cancel automatically. No dispose overrides needed.
listen: false — One-Time Read #
Pass listen: false to read a value without subscribing. The widget will not rebuild when the value changes:
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
final count = context.state(counterStore, listen: false);
print('Current count: $count');
},
child: const Text('Log'),
);
}
Firing Events #
Call context.fire(event, value) to dispatch an event through the nearest Caffeine ancestor's scope:
class CounterButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(children: [
IconButton(
onPressed: () => increment(Caffeine.of(context)),
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () => decrement(Caffeine.of(context)),
icon: const Icon(Icons.remove),
),
]);
}
}
If the event is bound to an ancestor scope, the fire automatically routes there and broadcasts through the subtree — no extra wiring needed.
Dependency Injection and Testing #
MappingStoreOverride replaces one store with another within a scope's subtree. Any context.state(originalStore) call inside transparently reads from the replacement:
await tester.pumpWidget(
Caffeine(
scopeFactory: (_) => Scope(overrides: {
MappingStoreOverride(from: apiStore, to: fakeApiStore),
}),
child: const FeatureWidget(),
),
);
FeatureWidget requires no modification — the override is fully transparent.
To isolate a store to a single widget subtree (e.g., multiple independent counters on the same screen), fork a scope with the store bound:
class CounterFeature extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Caffeine(
scopeFactory: (context) =>
Caffeine.of(context).fork(overrides: {counterStore}),
child: Builder(
builder: (context) {
final count = context.state(counterStore);
final doubled = context.state(doubledCounterValue);
// doubledCounterValue auto-promotes to live alongside counterStore.
return Column(/* ... */);
},
),
);
}
}