Small, composable primitives for reactive state, async task sequencing, DI, and lightweight widget rebuilding in Flutter.
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.
| 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. |
Add to pubspec.yaml:
dependencies:
stated: ^3.1.7 # use latestimport'package:flutter/material.dart';
import'package:stated/stated.dart';
// Immutable view stateclassCounterState {
constCounterState(this.count);
finalint count;
}
classCounterBlocextendsStated<CounterState> {
int _count =0;
voidincrement() =>notifyListeners(() => _count++); // calls buildState afterwards@overrideCounterStatebuildState() =>CounterState(_count);
}
voidmain() =>runApp(constMyApp());
classMyAppextendsStatelessWidget {
constMyApp({super.key});
@overrideWidgetbuild(BuildContext context) =>MaterialApp(
home:Scaffold(
body:Center(
child:StatedBuilder<CounterBloc>(
create: (_) =>CounterBloc(),
builder: (_, bloc, __) =>GestureDetector(
onTap: bloc.increment,
child:Text('Count: ${bloc.value.count}'),
),
),
),
),
);
}// StatedBuilder: rebuilds when Listenable changesStatedBuilder<MyBloc>(
create: (ctx) =>MyBloc(),
builder: (ctx, bloc, child) =>Text(bloc.value.title),
);
// Provide externally managed instanceStatedBuilder.value(existingBloc, builder: ...);
// Async creationFutureStatedBuilder<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, _) => ...,
);final counter =ValueEmitter<int>(0);
counter.addListener(() =>print('now: ${counter.value}'));
counter.value++; // triggersfinal 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 recomputefinal derived =LazyEmitter(() =>heavyCompute());
// attach derived.update to dependencies:
someListenable.addListener(derived.update);final todos =ListEmitter<String>();
todos.addListener(() =>print('changed: ${todos.length}'));
todos.add('Write docs');SubscriptionBuilder(
register: (sub) => sub
.add(counter, select: (_) => counter.value) // only when value changes
.add(todos, when: (l) => l.length.isOdd),
builder: (_, __) =>Text('reactive block'),
);classLoaderwithTasks, Dispose {
Future<void> loadFiles(List<String> ids) async {
for (final id in ids) {
awaitenqueue(() async { /* await network for id */ });
}
}
@overridevoiddispose() { cancelTasks(); super.dispose(); }
}Inside a cancellable task use the provided token:
awaitenqueueCancellable((token) async {
final data =awaitfetch();
token.ensureRunning(); // throws if cancelledprocess(data);
});final search =ValueEmitter('');
final runSearch =debounce(() { print('Query: ${search.value}'); }, constDuration(milliseconds:300));
search.addListener(runSearch);Register services:
final store =Store()
..add(Logger()) // instance
..addLazy<Database>((r) async=>Database()) // lazy singleton (async ok)
..addTransient<HttpClient>((l) =>HttpClient());// new each callawait store.init(); // pre-warm lazy (optional)final logger = store.get<Logger>(); // sync (must be initialised)final db =await store.resolve<Database>(); // safe asyncSupport async init phase:
classSessionManagerwithAsyncInit {
Future<void> init() async { /* load tokens */ }
}
store.addLazy<SessionManager>((r) async=>SessionManager());sealedclassAppEvent {}
classUserLoggedInextendsAppEvent { UserLoggedIn(this.userId); finalString userId; }
classUserLoggedOutextendsAppEvent {}
final events =Publisher<AppEvent>();
events.on<UserLoggedIn>().addListener(() =>print('login event'));
events.publish(UserLoggedIn('42'));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
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:
classFlagextendsStated<bool> { bool _v=false; voidtoggle()=>notifyListeners(()=>_v=!_v); @overrideboolbuildState()=>_v; }| 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.
final a =ValueEmitter(0);
final b =ValueEmitter(0);
final sum =Emitter.map([a, b], () => a.value + b.value);onChanged: (value) { text.value = value; }, // where text is ValueEmitter<String>
text.addListener(debounce(() =>search(text.value)));classLoaderwithTasks, Dispose {
Future<void> refresh() =>enqueue(() async { /* network */ });
@overridevoiddispose() { cancelTasks(); super.dispose(); }
}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.
Issues & PRs welcome. Please keep features focused & composable.
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
MIT - see LICENSE
See CHANGELOG.md
If this library helps you, consider starring the repo so others can find it.