scoped_store 0.1.0 copy "scoped_store: ^0.1.0" to clipboard
scoped_store: ^0.1.0 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
0
points
15
downloads

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)
View/report issues

License

unknown (license)

Dependencies

flutter, get_it

More

Packages that depend on scoped_store