scoped_store 0.2.1 copy "scoped_store: ^0.2.1" to clipboard
scoped_store: ^0.2.1 copied to clipboard

Minimal reactive-store + scoped dependency injection for Flutter. Retain and release stores by reference count, wire them into widgets with one line, rebuild on value change. Five files, zero codegen.

scoped_store #

Scoped dependency injection for Flutter with reference-counted store lifecycle. Three files, no codegen, no opinions about how your state updates.

What it solves #

You've probably shipped this bug before: a store is shared between two screens, the first screen disposes it, the second one keeps trying to read it. Or — the opposite — you keep the store alive forever because "something might still need it" and pay the memory cost.

scoped_store fixes this with reference counting:

  • retain<T> increments a counter and returns the instance (creating it the first time).
  • release<T> decrements. When the counter hits zero the store is disposed and unregistered automatically.

Every widget that needs a store retains on mount and releases on unmount. The store lives exactly as long as something needs it — no earlier, no later.

Install #

dependencies:
  scoped_store: ^0.1.0
import 'package:scoped_store/scoped_store.dart';

Three-step example #

1. Define a store #

Any class. Use whatever reactive primitive you like — ChangeNotifier, ValueNotifier, Riverpod, Bloc, signals. This package doesn't care.

class CounterStore implements Disposable {
  final count = ValueNotifier<int>(0);
  void increment() => count.value++;

  @override
  void dispose() => count.dispose();
}

Implementing Disposable is optional but recommended — StoreManager calls dispose() automatically when the last retainer releases. (Any class with a dispose() method also works via duck-typing; Disposable just makes the intent explicit.)

2. Retain and read #

// Registers + creates the first time. Subsequent calls bump the ref count.
retain<CounterStore>(() => CounterStore());

// Global shorthand for StoreManager.instance.get<T>():
get<CounterStore>().increment();

// Safe version — returns null instead of throwing:
tryGet<CounterStore>()?.increment();

// Release when done. If this was the last retainer, the store is disposed.
release<CounterStore>();

3. Bind to a widget subtree #

StoreBuilder retains on mount, releases on dispose. Pair it with any reactive builder — ValueListenableBuilder, ListenableBuilder, Riverpod, etc.

StoreBuilder<CounterStore>(
  create: () => CounterStore(),
  builder: (context, store) => ValueListenableBuilder(
    valueListenable: store.count,
    builder: (_, value, __) => Text('$value'),
  ),
);

Omit create: to look up an already-retained store.

Multiple instances by id: StoreRegistry #

When you need N instances of the same type keyed by something runtime — per user, per document, per tab.

// Fetch or create. Returns the existing instance if one already exists for
// this id; otherwise runs the factory.
final cart = StoreRegistry.instance.get<UserCart>(
  'user-42',
  () => UserCart('user-42'),
);

// Ref-counting is per (type, id):
StoreRegistry.instance.retain<UserCart>('user-42');
StoreRegistry.instance.release<UserCart>('user-42'); // disposed when 0

Lazy singletons #

For stores that should only be created if someone asks for them — not up-front at startup:

StoreManager.instance.registerFactory<ExpensiveStore>(
  () => ExpensiveStore.loadFromDisk(),
);

// Construction is deferred until the first get<ExpensiveStore>() call.
final store = get<ExpensiveStore>();

Why not just get_it? #

scoped_store is built on top of get_it and uses it as the underlying container — you get get_it's speed and ergonomics. What this package adds:

  • Reference counting. Two widgets can both "own" a store; the last one out turns off the lights. Plain get_it treats every registration as exclusive — you're responsible for ordering retain/release correctly.
  • Widget-scoped lifecycle via StoreBuilder — retain/release happens in initState / dispose so you can't forget.
  • Keyed registry for multi-instance patterns without the get_it factory-by-name boilerplate.

If you only ever register singletons for the entire app lifetime, plain get_it is perfect and you don't need this. The moment you have stores that should come and go with navigation, this fits.

API #

Type / function Purpose
StoreManager.instance The singleton registry. Usually accessed via shortcuts.
retain<T>([factory]) Create-or-increment. Returns the instance.
get<T>() Lookup; throws if not registered (or lazy-registers via factory).
tryGet<T>() Same as get but returns null instead of throwing.
release<T>() Decrement; dispose + unregister if count hits 0.
StoreManager.registerFactory<T>(fn) Register a lazy factory without creating yet.
StoreManager.getRefCount<T>() Inspect current ref count (debug / tests).
StoreManager.reset() Clear everything (tests).
StoreBuilder<T> Widget that retains for its subtree, releases on dispose.
StoreRegistry.instance.get<T>(id, factory) Keyed fetch-or-create.
StoreRegistry.instance.retain<T>(id) / release<T>(id) Keyed ref-count.
Disposable Opt-in interface so dispose() gets called on release.

Gotchas #

  • retain<T> without a factory throws if the type isn't already registered. Either pass a factory, or call registerFactory<T>(...) first.
  • StoreBuilder with create: owns the store instance — don't also retain it manually from initState of the same widget, or ref counts won't balance.
  • StoreManager.reset() is destructive and intended for tests. In app code you want targeted release<T>() calls.
  • All debug [StoreManager] ... logs are gated by assert — they compile out of release builds entirely.

License #

MIT

1
likes
130
points
15
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Minimal reactive-store + scoped dependency injection for Flutter. Retain and release stores by reference count, wire them into widgets with one line, rebuild on value change. Five files, zero codegen.

Repository (GitHub)

License

MIT (license)

Dependencies

flutter, get_it

More

Packages that depend on scoped_store