Skip to content

Repository files navigation

stated

Small, composable primitives for reactive state, async task sequencing, DI, and lightweight widget rebuilding in Flutter.

pub versionlicense

✨ 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 (Tasks mixin)
  • 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

ConceptSummary
Stated<T>Lazily builds a value via buildState(). Notifies only if value changes.
StatedBuilderCreates & listens to a Listenable (disposes if disposable).
FutureStatedBuilderAwaits async creation of a Stated before building.
BlocBuilderSimple create-once builder for any object (optionally disposable).
EmitterMixin 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.mapCombine multiple Listenables into a derived ValueListenable.
ListEmitter<T>A List<T> implementation emitting on structural changes.
SubscriptionAggregate multiple listenables with optional select & when filters.
TasksSequential async queue with cancellation tokens.
debounce()Wraps a callback with delayed execution.
StoreRegister: direct instance, lazy async, transient factory. Resolve via get<T>() or resolve<T>().
AsyncInitOptional mixin for async post-construction initialisation in lazy factories.
Publisher<T>Fire events & listen by subtype.
UriParser / PathMatcherDeclarative 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 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}'),
),
),
),
),
);
}

🧩 Builders

// 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, _) => ...,
);

🔄 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)

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);
});

🛰️ Debounce

final search =ValueEmitter('');
final runSearch =debounce(() { print('Query: ${search.value}'); }, constDuration(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 callawait 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:

classSessionManagerwithAsyncInit {
Future<void> init() async { /* load tokens */ }
}
store.addLazy<SessionManager>((r) async=>SessionManager());

📣 Publisher (Event Bus)

sealedclassAppEvent {}
classUserLoggedInextendsAppEvent { UserLoggedIn(this.userId); finalString userId; }
classUserLoggedOutextendsAppEvent {}
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:

classFlagextendsStated<bool> { bool _v=false; voidtoggle()=>notifyListeners(()=>_v=!_v); @overrideboolbuildState()=>_v; }

🆚 Comparison (High Level)

LibraryFocusPhilosophy
providerDI + InheritedWidget-driven
riverpodCompile-safe reactive graphOpinionated, layered
blocEvent/state patternStructured flows
statedTiny primitivesCompose 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

classLoaderwithTasks, Dispose {
Future<void> refresh() =>enqueue(() async { /* network */ });
@overridevoiddispose() { 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 Stated trees)
  • 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.

About

final state

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages