flutter_caffeine 1.0.0 copy "flutter_caffeine: ^1.0.0" to clipboard
flutter_caffeine: ^1.0.0 copied to clipboard

Flutter bindings for caffeine. Attach reactive scopes to the widget tree, read state with context.state(), and subscribe to automatic rebuilds — no StreamBuilders, no dispose boilerplate.

flutter_caffeine #

Flutter bindings for caffeine — a reactive microstore with managed side effects. flutter_caffeine attaches caffeine scopes to the widget tree and wires Stateful subscriptions directly into BuildContext, with no StreamBuilders, no dispose overrides, and no boilerplate wrapper widgets.


Table of Contents #


Installation #

dependencies:
  caffeine: ^1.0.0
  flutter_caffeine: ^0.0.1
import 'package:caffeine/caffeine.dart';
import 'package:flutter_caffeine/flutter_caffeine.dart';

How It Works #

flutter_caffeine has two building blocks:

  • Caffeine — a widget that creates and attaches a caffeine Scope to a point in the element tree, making it available to all descendant widgets. The scope is created once in initState via a scopeFactory callback and disposed automatically when the element leaves the tree.
  • context.state(node) — a BuildContext extension that reads a Stateful or Store value from the nearest Caffeine ancestor. When listen: true (the default), the widget rebuilds automatically whenever the value changes. Subscriptions are cleaned up via a Finalizer on the context — no dispose logic, no leaks.
  • context.fire(event) — a BuildContext extension that dispatches an event through the nearest Caffeine ancestor's scope.

The Caffeine Widget #

Root Scope Setup #

Place a Caffeine widget near the root of your app to create the root scope. The scopeFactory callback receives the BuildContext (useful for forking — see below) and returns the Scope to attach:

void main() {
  runApp(
    Caffeine(
      scopeFactory: (_) => Scope(),
      child: MyApp(),
    ),
  );
}

To apply global StoreOverrides for dependency injection, pass them when constructing the scope:

Caffeine(
  scopeFactory: (_) => Scope(references: {
    StoreOverride(analyticsStore, productionAnalyticsStore),
  }),
  child: MyApp(),
)

Forking into a Child Scope #

Use Caffeine.of(context) inside scopeFactory to fork a child scope from the parent. Stores bound to the child scope live only as long as the Caffeine widget:

// Wraps a screen — screenStore is disposed when the screen leaves the tree
Caffeine(
  scopeFactory: (context) => Caffeine.of(context).fork(references: {
    screenStore,
  }),
  child: ScreenWidget(),
)

Because scopeFactory receives the BuildContext at initState time, it can traverse any number of ancestor scopes before forking:

Caffeine(
  scopeFactory: (context) => Caffeine.of(context).fork(references: {
    modalStore,
    StoreOverride(authStore, guestAuthStore),
  }),
  child: ModalContent(),
)

Typical scope structure in a Flutter app:

Caffeine (root)      ─── overrides only, app lifetime
    │
    ├── Caffeine     ─── homeStore, home screen lifetime
    │
    └── Caffeine     ─── profileStore, avatarStore, profile screen lifetime
            │
            └── Caffeine  ─── editFormStore, edit modal lifetime

Reading State in Widgets #

context.state() — Subscribe and Rebuild #

Call context.state(node) inside build to read a Stateful or Store value. With listen: true (the default), the widget rebuilds whenever the value changes:

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final state = context.state(counter);
    return Text('${state.count}');
  }
}

Subscriptions are established once per node per element and deduplicated across rebuilds. Cleanup happens via a Finalizer registered on the BuildContext — when the element leaves the tree and is garbage collected, the finalizer fires and cancels the subscription automatically.

You can read multiple nodes in one build. Each call independently subscribes:

Widget build(BuildContext context) {
  final user    = context.state(userStore);
  final config  = context.state(remoteConfig);
  final summary = context.state(systemState); // Stateful combining both

  return Column(children: [
    Text(user.firstName),
    Text(config.apiUrl),
    Text('${summary.doubledMessages} messages'),
  ]);
}

Because caffeine compresses diamond updates, systemState triggers at most one rebuild per event cycle even when both of its upstream stores change simultaneously.

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 current = context.state(counter, listen: false);
      print('Tapped with count: ${current.count}');
    },
    child: const Text('Log count'),
  );
}

Firing Events #

context.fire() — Dispatch an Event #

Call context.fire(event) to dispatch an event to its target store through the nearest Caffeine ancestor's scope:

class CounterButtons extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(children: [
      IconButton(
        onPressed: () => context.fire(counter(CounterEvent.increment)),
        icon: const Icon(Icons.add),
      ),
      IconButton(
        onPressed: () => context.fire(counter(CounterEvent.decrement)),
        icon: const Icon(Icons.remove),
      ),
    ]);
  }
}

Dependency Injection and Testing #

StoreOverride replaces one store with another within the scope's subtree. Any context.state(originalStore) call inside that subtree transparently reads from the replacement:

await tester.pumpWidget(
  Caffeine(
    scopeFactory: (_) => Scope(references: {
      StoreOverride(analyticsStore, fakeAnalyticsStore),
      StoreOverride(apiStore, mockApiStore),
    }),
    child: FeatureWidget(),
  ),
);

FeatureWidget and everything inside it requires no modification — the override is fully transparent.

0
likes
125
points
99
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter bindings for caffeine. Attach reactive scopes to the widget tree, read state with context.state(), and subscribe to automatic rebuilds — no StreamBuilders, no dispose boilerplate.

Repository (GitHub)
View/report issues

Topics

#state-management #reactive #store #flutter

License

MIT (license)

Dependencies

caffeine, flutter

More

Packages that depend on flutter_caffeine