stated 3.1.6 copy "stated: ^3.1.6" to clipboard
stated: ^3.1.6 copied to clipboard

A neat set of tools aimed at making the state management easier.

stated #

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

pub version license

✨ 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 #

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.6 # 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 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.

8
likes
0
points
21
downloads

Publisher

verified publisherswipelab.co

Weekly Downloads

A neat set of tools aimed at making the state management easier.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on stated