all_observer 1.3.0
all_observer: ^1.3.0 copied to clipboard
Lightweight reactive state management for Flutter with zero external dependencies: observable values plus an auto-tracking Observer widget.
all_observer #
π§π· Leia em PortuguΓͺs
Reactive state for Flutter, zero dependencies. Observable values plus an
auto-tracking Observer widget β a small, safe, dependency-free core for
apps that want reactivity without a full state-management framework.
Why all_observer #
- Zero dependencies. The whole reactive core β tracking, notification,
collections, workers β is built on
Dart/Flutteralone. NoStream, no code generation, no external package to keep in sync with your Flutter version. - No boilerplate. No providers to register, no context to wire up, no
base classes to extend.
final count = 0.obs;plusObserver(() => ...)is a complete, working reactive pair. - Granular by construction. Dependencies are discovered by reading
.valueduring a build, not declared up front, so anObserveronly rebuilds for what it actually reads β including conditional branches (if (a) read x else read y), which are re-evaluated correctly on every build. - Safe by default. Rebuilds guard against unmounted widgets, a
synchronous update cycle (A β B β A) stops instead of overflowing the
stack, an exception inside one listener never stops the others, and
every misuse case (empty
Observer, write during build, write afterclose()) warns instead of crashing your app β with an opt-instrictModethat turns those same warnings into hard failures for CI. - Interoperable, not a walled garden.
Observable<T>is aValueListenable<T>, so it drops straight intoValueListenableBuilder,AnimatedBuilder,Listenable.merge, or any existing Flutter API that already understands that interface.
When to reach for it #
Small to medium apps and features that want local/global reactive state β counters, form fields, loading flags, a reactive list/cache, a computed summary β without adopting a full architecture (BLoC-style event/state plumbing, code-generated providers, etc.). It also works well alongside a larger architecture, as the reactive primitive under a view-model or controller class.
When something else may fit better #
If you need dependency injection, route-based state scoping, or
compile-time-checked provider graphs, a dedicated DI/state framework will
give you more structure than this package intentionally provides.
all_observer has no opinion on where your state lives β only on how it
notifies β so it composes with those frameworks rather than replacing
them (e.g. wrap an Observable inside a provider/service you already
manage).
Quick start (30 seconds) #
import 'package:all_observer/all_observer.dart';
final count = 0.obs; // ObservableInt
Observer(() => Text('${count.value}'));
count.value++; // rebuilds the Text above, and only that widget
Create observables from any type with .obs: 0.obs, 'hi'.obs,
false.obs, 9.99.obs, <String>[].obs, or wrap a custom type with
Observable<User?>(null, name: 'user'). Read .value inside an Observer
builder and the widget rebuilds automatically whenever it changes β
dependencies are re-discovered on every build, so conditional reads work
out of the box.
ValueListenable interop #
Every Observable<T> implements ValueListenable<T>, so it plugs directly
into anything that already speaks that interface β no adapter needed:
ValueListenableBuilder<int>(
valueListenable: count, // an Observable<int> works here directly
builder: (context, value, _) => Text('$value'),
);
AnimatedBuilder(animation: Listenable.merge([count, otherObservable]), ...);
Derived values with Computed #
final firstName = 'Carlos'.obs;
final lastName = 'Castro'.obs;
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
Observer(() => Text(fullName.value)); // recomputes only when needed
Computed<T> is lazy (never runs before the first read), memoized
(cached until a dependency notifies), reuses the same tracking mechanism
as Observer (so conditional/dynamic dependencies work the same way),
and only notifies its own listeners when the recomputed value actually
differs from the previous one. Call close() to unsubscribe from all
current dependencies.
For a narrower derived value from a single Observable, select is sugar
over the same pattern: user.select((u) => u.name) is exactly
Computed(() => user.value.name). The caller owns and must close() the
returned Computed.
Custom equals on Computed #
final fahrenheit = Computed<double>(
() => celsius.value * 9 / 5 + 32,
equals: (a, b) => (a - b).abs() < 0.01,
);
Like Observable, Computed accepts an equals override to decide
whether a recomputed value actually changed and should notify β useful for
floating-point tolerances or partial-field comparisons.
Diamond dependency graphs and batch #
A "diamond" is two Computeds both derived from the same source, with a
third depending on both. Wrap the writes that feed such a graph in
Observable.batch(): the recompute of any Computed in the graph is
deferred until every write in the batch has settled, so it recomputes at
most once, always from fully consistent upstream values. See "Known
limitations" below for what happens without batch.
Coalescing writes with Observable.batch #
Observable.batch(() {
firstName.value = 'Carlos';
lastName.value = 'Castro';
age.value = 30;
}); // manual listen()/ever() listeners fire exactly once, at the end
Writes still apply immediately and consistently inside the callback β
only the notification to manual subscribers (listen, ever, etc.) is
deferred and deduplicated. An Observer widget already coalesces
multiple dependency changes into a single rebuild per frame on its own,
so batch() mainly matters for manual subscriptions. Nested batch()
calls are supported; if the callback throws, the pending notifications
built up so far are discarded and the exception propagates normally.
Custom equality with equals #
final price = Observable<double>(
9.99,
equals: (a, b) => (a - b).abs() < 0.01,
);
By default, a write only notifies when the new value differs from the
current one via ==. Pass equals to use a different comparison β for
example, a tolerance for floating-point values, or comparing only part of
a larger object.
Reactive collections #
final items = <String>[].obs; // ObservableList<String>
Observer(() => Text('${items.length} items'));
items.add('one'); // notifies once
items.addAll(['two', 'three']); // still notifies once, not three times
items.removeWhere((e) => e == 'two'); // once, and only if something matched
ObservableList/ObservableMap/ObservableSet behave like their
built-in counterparts (ListBase/MapBase/SetBase) for every read β
length, [], contains, iteration β while every mutating member
notifies at most once per call, never once per element. A no-op
mutation (adding a Set element that's already there, removeWhere that
matches nothing, assigning an identical value to an existing map key)
notifies zero times.
Async values with ObservableFuture #
final userFuture = ObservableFuture<User>(() => api.fetchUser(id));
Observer(() => userFuture.value.when(
loading: (previousData) => const CircularProgressIndicator(),
data: (user) => Text(user.name),
error: (error, stackTrace) => Text('Error: $error'),
));
userFuture.refresh(); // re-runs the future, e.g. for pull-to-refresh
ObservableFuture<T> is an Observable<AsyncState<T>> that runs a
Future<T> Function() and tracks its loading/data/error lifecycle
automatically (autoStart: true by default; pass false and call run()
manually otherwise). AsyncLoading.previousData carries the last known
value while a refresh() is in flight, for stale-while-loading UIs. Every
run()/refresh() call is race-safe: if a newer call starts before an
older one resolves, the older result (success or error) is discarded when
it eventually arrives, and any in-flight result is also discarded if
close() was called meanwhile.
Beyond Observer: effects, streams, inspection, and lifecycle helpers #
A set of smaller, independent building blocks β each optional, none changing the core reactive graph.
Standalone reactivity with effect() #
final dispose = effect(() {
print('count is now ${count.value}');
});
// ...
dispose(); // stop reacting
effect() runs immediately and again whenever any observable it reads
changes β the same auto-tracking Observer uses, without a widget. Useful
outside the widget tree (a controller class, a background listener).
Escape hatches: untracked(), peek(), previousValue #
final result = untracked(() => a.value + b.value); // read without tracking
final current = counter.peek(); // read without tracking, shorter form
final before = counter.previousValue; // value right before the last change
untracked() reads observables inside its callback without registering
them as dependencies of whatever Observer/Computed/effect() is
currently tracking β useful for a one-off read that shouldn't cause a
rebuild on its own. Observable.peek() is sugar for the same thing on a
single value; previousValue looks at the value from before the most
recent update, with no extra bookkeeping to opt into.
Pluggable observability with ObserverInspector #
final recorder = RecordingInspector();
ObserverConfig.inspectors.add(recorder);
// ... later
for (final event in recorder.events) {
print(event); // ObservableCreateEvent, ObservableUpdateEvent, ...
}
Every creation/update/dispose/tracking/warning/effect-run event is exposed
through the ObserverInspector interface, not just printed to the console.
The classic colored terminal output is itself a formal implementation β
ConsoleInspector β registered internally by default; RecordingInspector
ships as an in-memory audit trail. Write your own to forward events to
analytics, a custom log sink, or a debug overlay β an exception thrown by
one inspector never blocks the others.
ObservableStream, the Stream counterpart of ObservableFuture #
final ticks = ObservableStream<int>(
() => Stream.periodic(const Duration(seconds: 1), (i) => i),
);
Observer(() => ticks.value.when(
loading: (previousData) => const CircularProgressIndicator(),
data: (n) => Text('$n'),
error: (error, stackTrace) => Text('Error: $error'),
));
Same AsyncState/loading-data-error contract as ObservableFuture, driven
by a Stream instead: every event becomes AsyncData, a stream error
becomes AsyncError, and refresh() cancels the current subscription and
starts a fresh one β race-safe via the same generation-counter guard.
ObserverStateMixin, for side effects tied to a State's lifetime #
class _MyPageState extends State<MyPage> with ObserverStateMixin {
@override
void initState() {
super.initState();
autorun(() {
if (session.value.isExpired) {
Navigator.of(context).pushReplacementNamed('/login');
}
});
}
}
autorun is effect() auto-disposed with the State; autoDispose takes
any Disposer (a subscription's .cancel, a Computed's .close, ...).
This is for side effects that don't belong in build() β navigation,
snackbars, driving an AnimationController β not a replacement for
Observer.
Optional persistence with ObservableStore #
final theme = Observable<String>('light');
final stop = theme.persistWith(myThemeStore); // myThemeStore: ObservableStore<String>
// ...
stop(); // stop persisting; `theme` keeps working normally
ObservableStore<T> is a three-method (read/write/delete) interface
with no implementation shipped here β all_observer stays dependency-free.
A bridge package (e.g. all_box) can
implement it against real storage; persistWith restores once on binding
and writes back on every subsequent change.
Bounded undo/redo with ObservableHistory #
final text = Observable<String>('');
final history = text.withHistory(limit: 50);
text.value = 'hello';
text.value = 'hello world';
history.undo(); // text.value == 'hello'
history.undo(); // text.value == ''
history.redo(); // text.value == 'hello'
history.dispose();
Records every value change, skips changes made by undo()/redo()
themselves (so redoing after undoing restores the exact value, instead of
creating a new branch), and drops the oldest entries once limit is
exceeded.
package:all_observer/core.dart β the pure-Dart engine, without Flutter #
import 'package:all_observer/core.dart';
final counter = CoreObservable<int>(0);
counter.addListener(() => print('now ${counter.value}'));
counter.value = 1;
The dependency tracker, listener registry, batch/flush engine, and
observability types have zero import of package:flutter and are
re-exported through this separate entry point β usable from a CLI tool, a
server, or a background isolate, not just a Flutter app. Observable/
Computed (from all_observer.dart) are thin ValueListenable + console-
logging wrappers over CoreObservable/CoreComputed β same engine, same
behavior, Flutter added on top.
Fine-grained rebuilds with Observer.withChild #
Observer.withChild(
builder: (context, child) => Row(
children: [Text('${count.value}'), child],
),
child: const ExpensiveStaticWidget(),
);
A static child subtree, a common technique for avoiding rebuilds of
expensive widgets that don't depend on any observable: child is built
once and passed back into builder on every rebuild instead of being
reconstructed.
setValue, an unambiguous way to assign null #
final name = Observable<String?>('Carlos');
name.setValue(null); // assigns null and notifies
call() treats a null argument as "no argument" (to support the
no-argument observable() read form), so observable(null) reads instead
of assigning. setValue(newValue) is equivalent to value = newValue and
assigns null unambiguously; it is also handy as a tear-off (e.g. directly
as an onChanged callback).
Local, self-contained state with ObserverValue #
ObserverValue<ObservableInt>(
(data) => ElevatedButton(
onPressed: () => data.value++,
child: Text('${data.value}'),
),
0.obs,
);
A thin convenience over Observer for state that's created and consumed
right where it's used: pass the observable in, get it back inside
builder on every rebuild β no separate variable to declare above the
widget, no explicit Observer(() => ...) wrapping.
Side effects with workers #
final query = ''.obs;
final isLoggedIn = false.obs;
final count = 0.obs;
final scrollOffset = 0.0.obs;
// Runs 400ms after the last change β perfect for search-as-you-type.
final debounceWorker = debounce(query, (String value) {
runSearch(value);
}, time: const Duration(milliseconds: 400));
// Runs once, then disposes itself automatically.
once(isLoggedIn, (bool value) {
if (value) analytics.logLogin();
});
// Runs on every change, like a manual listener with a friendlier name.
final everWorker = ever(count, (int value) => print('count is now $value'));
// Runs at most once per `time`, immediately on the first change.
final intervalWorker = interval(scrollOffset, (double value) {
saveScrollPosition(value);
}, time: const Duration(seconds: 1));
// Dispose whichever ones you keep a reference to when you're done:
Workers([debounceWorker, everWorker, intervalWorker]).dispose();
Workers are the recommended way to run non-widget side effects (network
calls, analytics, persistence) off an observable change, instead of
sprinkling addListener calls by hand. once disposes itself after
firing; debounce/interval cancel their internal Timer on dispose()
so nothing fires after you're done with them.
Colored debug logs #
Enable ObserverConfig.logging = true during development to see reactivity
happen in your terminal, color-coded by event type:
| Event | Color |
|---|---|
| β creation | green |
| β» value update | cyan (values in magenta) |
| π Observer tracking | blue |
| β dispose | gray |
| β misuse warning | bold yellow |
[all_observer] β Observable<int>(count) criado β 0
[all_observer] β» Observable<int>(count): 0 β 1
[all_observer] π Observer(contador) rastreando: [count, isLoading]
[all_observer] β Observable<int>(count) descartado (2 listeners removidos)
Set ObserverConfig.useColors = false on terminals without ANSI support.
Misuse warnings (an Observer that reads nothing, a write after close(),
a write during build, a probable listener leak) are on by default via
ObserverConfig.warnings and never crash the app β set strictMode = true
to turn the "empty Observer" case into an exception for CI/tests.
Design decisions #
Rebuilds are guarded against already-unmounted widgets: the internal
callback checks mounted before scheduling work, and defers to the next
frame instead of a bare microtask when a change happens mid-build. Nested
reactive builders are supported correctly through a stack-based dependency
tracker, rather than a single mutable "current context" that nested
tracking could clobber. Notification semantics are a single, predictable
rule β a write only notifies if the new value differs from the current one
β with no special-casing for first assignment; mutable objects changed in
place can force a notification via refresh(). Equality (==/hashCode)
is never overridden on the reactive wrapper, so comparisons always mean
what they say: compare .value explicitly. The core has no Stream or
StreamController inside it β listen() is built directly on top of a
lightweight listener registry, keeping the reactive core small. And rather
than throwing on likely mistakes, the package favors friendly, non-fatal
warnings by default, with an opt-in strict mode for teams that want hard
failures in CI.
More #
ObservableList,ObservableMap,ObservableSet: reactive collections; reading any member tracks it, mutating any member notifies exactly once per call (bulk operations likeaddAll/removeWhere/retainWherenever notify per element).Computed<T>: lazy, memoized derived values built on the same dependency tracker asObserver, with an optional customequalsand batch-aware diamond-glitch mitigation.ObservableFuture<T>/ObservableStream<T>/AsyncState<T>(aliasedAsyncValue<T>): race-safe async loading/data/error state built onObservable, driven by aFutureor aStream.effect()/untracked()/Observable.peek()/Observable.previousValue: standalone reactivity and tracking escape hatches.ObserverInspector/ConsoleInspector/RecordingInspector: pluggable observability for every lifecycle/update/warning event.ObserverStateMixin: auto-disposedeffect()s and manual subscriptions tied to aState's lifetime.ObservableStore<T>/Observable.persistWith: optional persistence integration point (e.g. forall_box), no dependency added.ObservableHistory<T>/Observable.withHistory: bounded undo/redo for anyObservable.package:all_observer/core.dart: the pure-Dart engine (CoreObservable,CoreComputed,DependencyTracker, ...), zeropackage:flutterimport, usable outside Flutter.Observable.batch: coalesces multiple writes into one notification per changed observable, for manual subscribers.Observer.withChild: rebuilds only the builder-owned part of a subtree, reusing a staticchildacross rebuilds.Observable.select: sugar for a narrowerComputedderived from oneObservable.ObserverValue<T>: local, self-contained reactive state without managing an observable's lifecycle separately.ever,once,debounce,interval: workers for side effects driven by observable changes.- A synchronous update cycle (A's listener writes B, B's listener writes A, ...) is stopped after a bounded notification depth with a descriptive error, instead of a raw stack overflow; an exception thrown inside one listener never stops the other listeners of the same observable from running.
- See
/examplefor a runnable demo (counter, reactive list, worker, debug-log toggle), and/benchmarkfor manual Stopwatch-based microbenchmarks.
How all_observer compares #
A factual, non-marketing comparison against other Flutter/Dart reactivity
approaches β what each one requires that all_observer doesn't, and vice
versa. None of these are "bad"; they solve for different priorities.
all_observer |
GetX | Riverpod | MobX | flutter_hooks | |
|---|---|---|---|---|---|
| External dependencies | Zero | Zero (itself is all-in-one) | riverpod, often flutter_riverpod/riverpod_generator |
mobx, mobx_codegen, build_runner |
flutter_hooks |
| Code generation | None | None | Optional (riverpod_generator), common in practice |
Required (build_runner) for @observable/@computed/@action |
None |
| Scope | Reactive values + widget rebuilding only | State + routing + DI + snackbars/dialogs (a full framework) | State + DI (provider graph), no routing/UI helpers | Reactive values + actions/reactions, no DI/routing | Widget-local composable state/lifecycle, no cross-widget store |
| Dependency tracking | Auto (read .value during build/effect()/Computed) |
Auto, via Obx/GetX reading .value/.obs |
Auto, via ref.watch inside a Provider/Notifier |
Auto, via Observer/reactions reading @observable fields |
N/A (hooks are widget-local, not a dependency graph) |
| Diamond-dependency glitches | Prevented by design (ADR-0001/ADR-0002 in ARCHITECTURE.md) |
Not a documented guarantee | N/A (providers don't form a Computed-chain graph the same way) |
Prevented by MobX's own reactive core | N/A |
ValueListenable interop |
Direct β Observable<T> is one |
No | No | No | No |
| Escape hatches | untracked(), .peek(), .previousValue |
.value reads outside Obx already untracked |
ref.read (untracked read) |
mobx.untracked |
N/A |
| Observability | Pluggable ObserverInspector (ConsoleInspector, RecordingInspector, or your own) |
Console logging via Get.log |
ProviderObserver |
MobX dev-tools / Spy API | N/A |
| Usable outside Flutter | Yes β package:all_observer/core.dart, zero Flutter import |
No | Yes (riverpod core has a non-Flutter variant) |
Yes (mobx core is pure Dart) |
No (Flutter-only, by design) |
| Failure mode for misuse | Friendly warning by default, opt-in strictMode throws |
Varies by API surface | Compile-time-checked provider graph catches some classes of mistake earlier | Runtime warnings in dev mode | Compile-time (hook rules enforced by lint) |
Why choose all_observer #
Reach for it when you want reactive state and nothing else: no DI
container to learn, no routing convention to adopt, no code generator in
your build pipeline, and no risk of a transitive dependency going stale or
unmaintained, because there isn't one. final count = 0.obs; plus
Observer(() => Text('${count.value}')) is the entire mental model β the
same primitive scales from a single counter to a Computed graph, race
-safe async state, and pluggable observability, without switching
vocabulary partway through. It composes with (rather than replaces) GetX's
routing/DI, Riverpod's provider graph, or a hand-rolled controller class,
since all_observer has no opinion on where state lives β only on how
it notifies.
Reach for something else when you specifically need what that something
else specializes in: GetX's all-in-one routing+DI+state if you want one
framework for everything; Riverpod if you want a compile-time-checked DI
graph and don't mind the provider-declaration ceremony; MobX if you're
already invested in its action/reaction vocabulary and codegen step;
flutter_hooks if your state is genuinely widget-local and you want
useState/useEffect-style composition instead of a standalone value.
Migrating from other reactive state solutions #
all_observer covers the same core concepts most reactive state
approaches do, under its own names. This is a concept-by-concept map, not
a name-for-name port of any specific library:
| Concept | all_observer |
|---|---|
| Rx-style reactive value | Observable<T> (.obs to create one) |
| Reactive builder widget, auto-tracking dependencies | Observer |
| Derived/computed reactive value | Computed<T> |
Side-effect helpers on value change (ever/once/debounce/interval) |
Same names: ever, once, debounce, interval |
| Async loading/data/error state | ObservableFuture<T> / AsyncState<T> |
| Coalesced/transactional writes | Observable.batch(() { ... }) |
| Dispose / teardown | close() on every Observable/Computed/collection |
What has no equivalent here, by design β all_observer only handles
reactivity, not app architecture:
- Routing / navigation: use Flutter's own
Navigator/Router, or a dedicated routing package. - Snackbars / dialogs / overlays: use Flutter's own
ScaffoldMessenger,showDialog,showModalBottomSheet, etc. directly βall_observerhas no UI-side-effect layer to hook into these. - Dependency injection / service location: bring your own DI solution
(a simple constructor-passed singleton, an
InheritedWidget, or a dedicated DI package) and storeObservables inside the services/ controllers it manages βall_observerhas no opinion on where state lives, only on how it notifies.
Known limitations #
Observable.batch()is a performance optimization, not a consistency requirement. Since v1.2.0 every write β even a standaloneobservable.value = xoutside any explicitbatch()β is automatically routed through the same two-phase flush thatbatch()uses. Diamond dependency graphs (ComputedA and B both derived from source S,ComputedC depending on A and B) always recompute exactly once, always from fully settled upstream values β no glitch, no batch required. Wrapping multiple writes inbatch()remains useful to coalesce notifications: all writes in the callback are committed first, then listeners are notified once per changed observable, instead of once per write.Computedstays subscribed after its first read, untilclose(). Reading.value(or attaching a listener) makes aComputedsubscribe to its current dependencies indefinitely β it does not unsubscribe itself just because nobody is listening anymore. Callclose()once you're done with aComputedyou created manually (short-lived ones, e.g. fromselect, are easy to forget).- Single-isolate confinement. Like the rest of Dart, every
Observable/Computed/collection is confined to the isolate that created it; there is no cross-isolate synchronization. UseSendPort/ReceivePortorcomputeto move data between isolates and write back to the observable on its own isolate.
Other packages by us #
all_observer is part of a small family of zero/low-dependency Dart &
Flutter packages published under the
opensource.tatamemaster.com.br
verified publisher:
all_validations_brβ Brazilian document validation (CPF, CNPJ, CNH, PIX), 23 input formatters/masks, and pure-Dart utilities (JWT, UUID, currency, ChaCha20-Poly1305/AES encryption).all_boxβ synchronous, lightweight key-value storage for Flutter with crash-safe writes (write-ahead + atomic rename) and a pure-Flutter reactive layer.all_image_compressβ pure-Dart image compression (JPEG, PNG, GIF, BMP, TIFF, WebP), running in isolates to avoid UI jank, no native code required.
π₯ Contributors #
Made with contrib.rocks.
Contributions are welcome! Read CONTRIBUTING.md to get started.
Issues and pull requests are welcome at the GitHub repository.