Small, composable primitives for reactive state, async task sequencing, DI, and lightweight widget rebuilding in Flutter.
β¨ What is this?
stated is a minimal toolkit that lets you build structure without ceremony:
- A
Stated<T>base for lazily computed immutable view state - Simple builder widgets (
StatedBuilder,FutureStatedBuilder,BlocBuilder) - Reactive primitives (
Emitter,ValueEmitter,LazyEmitter,ListEmitter) - Multi-source subscriptions (
Subscription/SubscriptionBuilder) - Async task sequencing & cancellation (
Tasksmixin) - Debouncing utilities (
debounce,Debouncer) - An ultraβlean service locator / DI container (
Store) with sync, lazy, transient and async init support - A small event bus (
Publisher) with type filtering - URI pattern parsing & canonicalisation (
UriParser,PathMatcher) - Deterministic resource disposal (
Dispose,Disposable)
You can adopt one piece at a time. Nothing forces a framework-wide migration.
π§ Core Concepts
| Concept | Summary |
|---|---|
Stated<T> |
Lazily builds a value via buildState(). Notifies only if value changes. |
StatedBuilder |
Creates & listens to a Listenable (disposes if disposable). |
FutureStatedBuilder |
Awaits async creation of a Stated before building. |
BlocBuilder |
Simple create-once builder for any object (optionally disposable). |
Emitter |
Mixin exposing notifyListeners(). Basis for all reactive primitives. |
ValueEmitter<T> |
Mutable value + change notifications. |
LazyEmitter<T> |
Computes value on demand & caches until dependencies trigger update. |
Emitter.map |
Combine multiple Listenables into a derived ValueListenable. |
ListEmitter<T> |
A List<T> implementation emitting on structural changes. |
Subscription |
Aggregate multiple listenables with optional select & when filters. |
Tasks |
Sequential async queue with cancellation tokens. |
debounce() |
Wraps a callback with delayed execution. |
Store |
Register: direct instance, lazy async, transient factory. Resolve via get<T>() or resolve<T>(). |
AsyncInit |
Optional mixin for async post-construction initialisation in lazy factories. |
Publisher<T> |
Fire events & listen by subtype. |
UriParser / PathMatcher |
Declarative path pattern matching with typed extraction. |
π Quick Start
Add to pubspec.yaml:
dependencies: stated: ^3.1.7 # use latest
Counter with Stated
import 'package:flutter/material.dart'; import 'package:stated/stated.dart'; // Immutable view state class CounterState { const CounterState(this.count); final int count; } class CounterBloc extends Stated<CounterState> { int _count = 0; void increment() => notifyListeners(() => _count++); // calls buildState afterwards @override CounterState buildState() => CounterState(_count); } void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => MaterialApp( home: Scaffold( body: Center( child: StatedBuilder<CounterBloc>( create: (_) => CounterBloc(), builder: (_, bloc, __) => GestureDetector( onTap: bloc.increment, child: Text('Count: ${bloc.value.count}'), ), ), ), ), ); }
π§© Builders
// StatedBuilder: rebuilds when Listenable changes StatedBuilder<MyBloc>( create: (ctx) => MyBloc(), builder: (ctx, bloc, child) => Text(bloc.value.title), ); // Provide externally managed instance StatedBuilder.value(existingBloc, builder: ...); // Async creation FutureStatedBuilder<MyState>( future: (ctx) async => MyAsyncBloc(), builder: (ctx, state, child) => Text(state.toString()), ); // Simple life-cycle wrapper (no listening) BlocBuilder<ExpensiveService>( create: (ctx) => ExpensiveService(), builder: (ctx, service, _) => ..., );
π Reactive Primitives
ValueEmitter
final counter = ValueEmitter<int>(0); counter.addListener(() => print('now: ${counter.value}')); counter.value++; // triggers
Derived values with Emitter.map + debounce
final a = ValueEmitter(1); final b = ValueEmitter(2); final sum = Emitter.map([a, b], debounce(() => a.value + b.value)); sum.addListener(() => print('sum: ${sum.value}')); a.value = 10; b.value = 30; // debounced single recompute
LazyEmitter manual invalidation
final derived = LazyEmitter(() => heavyCompute()); // attach derived.update to dependencies: someListenable.addListener(derived.update);
ListEmitter
final todos = ListEmitter<String>(); todos.addListener(() => print('changed: ${todos.length}')); todos.add('Write docs');
Subscription
SubscriptionBuilder( register: (sub) => sub .add(counter, select: (_) => counter.value) // only when value changes .add(todos, when: (l) => l.length.isOdd), builder: (_, __) => Text('reactive block'), );
β±οΈ Task Queue (Sequential Async)
class Loader with Tasks, Dispose { Future<void> loadFiles(List<String> ids) async { for (final id in ids) { await enqueue(() async { /* await network for id */ }); } } @override void dispose() { cancelTasks(); super.dispose(); } }
Inside a cancellable task use the provided token:
await enqueueCancellable((token) async { final data = await fetch(); token.ensureRunning(); // throws if cancelled process(data); });
π°οΈ Debounce
final search = ValueEmitter(''); final runSearch = debounce(() { print('Query: ${search.value}'); }, const Duration(milliseconds: 300)); search.addListener(runSearch);
π¦ Store (Service Locator / DI)
Register services:
final store = Store() ..add(Logger()) // instance ..addLazy<Database>((r) async => Database()) // lazy singleton (async ok) ..addTransient<HttpClient>((l) => HttpClient());// new each call await store.init(); // pre-warm lazy (optional) final logger = store.get<Logger>(); // sync (must be initialised) final db = await store.resolve<Database>(); // safe async
Support async init phase:
class SessionManager with AsyncInit { Future<void> init() async { /* load tokens */ } } store.addLazy<SessionManager>((r) async => SessionManager());
π£ Publisher (Event Bus)
sealed class AppEvent {} class UserLoggedIn extends AppEvent { UserLoggedIn(this.userId); final String userId; } class UserLoggedOut extends AppEvent {} final events = Publisher<AppEvent>(); events.on<UserLoggedIn>().addListener(() => print('login event')); events.publish(UserLoggedIn('42'));
π URI Parsing
final parser = UriParser<String, void>( routes: [ UriMap('/users/{id:#}', (m) => 'User #${m.pathParameters['id']}'), UriMap.many(['/posts/{slug:w}', '/blog/{slug:w}'], (m) => 'Post ${m.pathParameters['slug']}'), ], canonical: { 'lang': (raw) => switch(raw) { 'en-US' => 'en', 'en' => 'en', _ => null }, }, ); parser.parse(Uri.parse('/users/123'), null); // => 'User #123'
Patterns:
{field}word / dash / underscore{field:#}digits{field:w}word chars{field:*}greedy{*}wildcard segment
π§ͺ Testing Patterns
All primitives are pure Dart / Flutter-friendly. Example:
test('lazy factory resolves only once', () async { final store = Store(); var created = 0; store.addLazy<int>((r) async => ++created); expect(await store.resolve<int>(), 1); expect(await store.resolve<int>(), 1); });
For Stated just mutate via notifyListeners wrapper:
class Flag extends Stated<bool> { bool _v=false; void toggle()=>notifyListeners(()=>_v=!_v); @override bool buildState()=>_v; }
π Comparison (High Level)
| Library | Focus | Philosophy |
|---|---|---|
| provider | DI + Inherited | Widget-driven |
| riverpod | Compile-safe reactive graph | Opinionated, layered |
| bloc | Event/state pattern | Structured flows |
| stated | Tiny primitives | Compose only what you need |
Use stated when you want low ceremony & control, or to augment existing setups.
π³ Cookbook
Combine multiple counters into a derived state
final a = ValueEmitter(0); final b = ValueEmitter(0); final sum = Emitter.map([a, b], () => a.value + b.value);
Debounced text field
onChanged: (value) { text.value = value; }, // where text is ValueEmitter<String> text.addListener(debounce(() => search(text.value)));
Cancel pending tasks on dispose
class Loader with Tasks, Dispose { Future<void> refresh() => enqueue(() async { /* network */ }); @override void dispose() { cancelTasks(); super.dispose(); } }
β FAQ
Why not use ChangeNotifier directly? Stated<T> formalizes immutable snapshot building & avoids redundant notifications.
Does Store replace Provider? It is a minimal locatorβuse it alongside Provider if you like.
Is this production ready? The code is intentionally small; review and adopt incrementally.
π€ Contributing
Issues & PRs welcome. Please keep features focused & composable.
π§ Possible Next Steps / Roadmap
These are intentionally not included yet to keep scope tight, but may be explored:
- DevTools integration helpers (inspecting
Statedtrees) - Flutter extension widgets for Provider / InheritedWidget bridging
- Async task progress utilities (percent / state enum)
- Stream adapters (Emitter <-> Stream)
- Code generation for DI registration (optional layer)
- More collection emitters (
MapEmitter,SetEmitter) - Documentation site with interactive examples
- Lint rules to encourage immutable state models
π License
MIT - see LICENSE
π Changelog
See CHANGELOG.md
If this library helps you, consider starring the repo so others can find it.