all_observer
π§π· PortuguΓͺs | πΊπΈ English
Reactive state for Flutter with zero dependencies β final count = 0.obs; +
Observer(...) and you're done.

Table of contents
- Features
- Installing
- Typed observable aliases
- Custom logging / ObserverInspector
- Counter app step by step
- The building blocks
- Observer vs watch(context) β choosing the right one
- Comparison
- When to use it (and when not to)
- Documentation
- Other packages by us
Features
- πͺΆ Zero dependencies β the whole reactive core is built on
Dart/Flutteralone, nothing else to keep in sync with your Flutter version. - βοΈ No boilerplate, no code generation β
final count = 0.obs;plusObserver(() => ...)is a complete, working reactive pair. - π― Granular rebuilds β dependencies are discovered by reading
.valueduring a build, so only the widget that actually reads a value rebuilds. - π‘οΈ Safe by default β glitch-free diamond dependencies, race-safe async, unmounted-widget guards, and friendly warnings instead of crashes (with opt-in
strictModefor CI). - π§ͺ Testable by design β
Observable/Computedare plain Dart objects, no wrapper/DI required to test them; see Testing. - π
ValueListenableinterop βObservable<T>is aValueListenable<T>, so it drops straight intoValueListenableBuilder,AnimatedBuilder,Listenable.merge. - π©Ί Built-in colored debug logging β flip
ObserverConfig.logging = trueand watch every create/update/track/dispose event in your terminal.
Installing
flutter pub add all_observer
dependencies:
all_observer: ^1.5.6
import 'package:all_observer/all_observer.dart';
Typed observable aliases
ObsBool, ObsInt, ObsDouble, and ObsString are lightweight typedef
aliases for common Observable<T> types. They are only syntactic sugar: they
add no classes or behavior.
final loading = ObsBool(false, name: 'loading');
final count = ObsInt(0, name: 'count');
Observer(() => Text('${count.value}'));
count.value++;
Custom logging / ObserverInspector
ObserverInspector is the all_observer conceptual equivalent of bloc's
BlocObserver: a global, pluggable observability API for forwarding typed
lifecycle, update, dependency-tracking, warning, effect, and scope events to
your own logger, analytics service, or test audit trail.
Unlike bloc's single observer slot, ObserverConfig.inspectors is already a
list, so multiple inspectors can be registered directly. An exception from
one inspector is isolated: other inspectors and the reactive update continue
normally. Set ObserverConfig.captureStackTraces = true only while debugging
when event stack traces are needed.
The built-in ConsoleInspector remains controlled by
ObserverConfig.logging, warnings, and logLevel; custom inspectors are
additional sinks and do not duplicate, replace, or silence that console
output. RecordingInspector provides a bounded in-memory audit trail for
tests.
class AppObserverInspector extends ObserverInspector {
@override
void onCreate(ObservableCreateEvent event) {
logger.info('created ${event.label}');
}
@override
void onUpdate(ObservableUpdateEvent event) {
logger.info('${event.label}: ${event.oldValue} -> ${event.newValue}');
}
@override
void onWarning(WarningEvent event) {
logger.warning('${event.label}: ${event.suggestion ?? ''}');
}
@override
void onDispose(ObservableDisposeEvent event) {
logger.info('disposed ${event.label}');
}
}
void main() {
ObserverConfig.inspectors.add(AppObserverInspector());
runApp(const App());
}
Counter app step by step
Step 1 β Create an observable
final count = 0.obs; // ObservableInt
.obs wraps any value in an Observable β count now holds 0 and can be watched for changes.
Step 2 β Wrap your UI in an Observer
import 'package:flutter/material.dart';
import 'package:all_observer/all_observer.dart';
final count = 0.obs;
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Observer(() => Text('${count.value}')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++,
child: const Icon(Icons.add),
),
);
}
}
Run it inside any MaterialApp(home: CounterPage()).
Step 3 β Update the value
onPressed: () => count.value++,
Only the Observer(() => Text('${count.value}')) above rebuilds β nothing else in CounterPage re-renders, because it's the only widget that read count.value during its build.
Step 4 β Watch it happen
ObserverConfig.logging = true;
[all_observer] β Observable<int>(count) created β 0
[all_observer] π Observer(unnamed) tracking: [count]
[all_observer] β» Observable<int>(count): 0 β 1
The building blocks
Observable
Any value wrapped with .obs (or Observable<T>(initial) for custom types). Reading .value inside a tracked builder registers a dependency; writing only notifies when the new value differs.
final name = Observable<User?>(null, name: 'user');
name.value = User('Carlos');
Observer
Auto-tracking widget: dependencies are re-discovered on every build, so conditional reads work out of the box.
Observer(() => Text('${count.value}'));
watch(context) β no wrapper needed
Any widget can subscribe its own element directly from build(); only
that widget rebuilds when the value changes.
@override
Widget build(BuildContext context) => Text('${count.watch(context)}');
More about watch(context) (including its lazy-cleanup trade-off) here.
Computed
Lazy, memoized derived value, built on the same tracker Observer uses.
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
Observer(() => Text(fullName.value)); // recomputes only when needed
Reactive collections
ObservableList/ObservableMap/ObservableSet behave like their built-in counterparts, notifying at most once per mutating call.
final items = <String>[].obs;
Observer(() => Text('${items.length} items'));
items.addAll(['one', 'two']); // notifies once, not twice
items.insertAll(1, ['one and a half', 'one and three quarters']); // notifies once
final startsWithOne = items.where((e) => e.startsWith('one')); // read, no mutation
ObservableFuture (async state)
Runs a Future and tracks its loading/data/error lifecycle, with race-safe refreshes.
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'),
));
Workers
ever, once, debounce, interval β side effects driven by an observable change, without hand-rolled addListener calls.
final query = ''.obs;
final search = debounce(query, (String value) => runSearch(value),
time: const Duration(milliseconds: 400));
batch
Coalesces multiple writes so manual (listen/ever) subscribers notify once instead of once per write β Observer already coalesces rebuilds per frame on its own.
More about batch and diamond dependencies here.
Observer vs watch(context) β choosing the right one
Both use the exact same dependency tracker and re-discover dependencies on every build β neither is "smarter" than the other. The difference is where the subscription lives and how much code you write.
Quick-decision table
| Situation | Reach for |
|---|---|
Small leaf widget whose entire build() is reactive |
watch(context) |
One reactive slice inside a large build() (e.g. only a Text in a Scaffold) |
Observer(() => ...) |
| Expensive static subtree that should never rebuild | Observer.withChild(builder:, child:) |
| Long-lived global observable read by many short-lived screens | Observer (eager dispose()) |
Reactive value inside an Observer builder |
watch(context) β delegates to the enclosing context, no double subscription |
Side-by-side
// watch(context) β the widget subscribes itself, no wrapper
class CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) => Text('${count.watch(context)}');
}
// Observer β a wrapper widget owns the subscription
Observer(() => Text('${count.value}'));
Both rebuild only the widget that read the value. The practical difference shows up when the build() is large:
// watch rebuilds the whole Scaffold when count changes
Widget build(BuildContext context) {
final n = count.watch(context); // <-- subscribes this Element
return Scaffold(
body: Center(child: Text('$n')),
// ... 50 other widgets
);
}
// Observer rebuilds only the Text node
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Observer(() => Text('${count.value}')), // <-- only this rebuilds
),
// ... 50 other widgets
);
}
Cleanup behaviour
| Cleanup timing | |
|---|---|
Observer |
Immediate β dispose() unsubscribes on unmount |
watch(context) |
Lazy β the dead subscription is released on the first notification after unmount (guaranteed no-op: nothing rebuilds, nothing throws). On an observable that never changes again the inert listener persists β prefer Observer for long-lived globals. |
The rule of thumb
Leaf widget, whole
build()is reactive βwatch(context).
Reactive slice inside a bigbuild(), static subtree, or eager cleanup βObserver.
They are additive and compose freely. A watch call inside an Observer builder simply reads the value and delegates to the enclosing tracker β no extra subscription is created.
Comparison
| all_observer | GetX | Riverpod | Bloc | MobX | signals | |
|---|---|---|---|---|---|---|
| External dependencies | Zero | Zero (all-in-one) | riverpod (+ generator, common) |
bloc, flutter_bloc |
mobx, build_runner |
Zero |
| Code generation | None | None | Optional, common in practice | None | Required (@observable/@action) |
None |
| Boilerplate | Minimal (.obs + Observer) |
Minimal | Provider declarations | Events/states/handlers | Annotated store classes | Minimal |
| Rebuild granularity | Per-read, auto-tracked | Per-read, auto-tracked | Per ref.watch |
Per BlocBuilder/selector |
Per-read, auto-tracked | Per-read, auto-tracked |
| Learning curve | Low | Lowβmedium | Medium | Mediumβhigh | Medium | Low |
| Scope | Reactivity only | Full framework (state+routing+DI) | State + DI graph | State machine/event architecture | Reactivity + actions | Reactivity only |
all_observer intentionally doesn't do routing, DI, or snackbars β that's a
design choice, not a gap. Full, detailed comparison here.
Measured in 1.5.7: stable cached debug labels reduced untracked
CoreObservablereads in AOT by 98.8% and zero-listenerObservablewrites in Flutter/JIT by 95.4% on the same reference machine and harness. Results vary by environment; see the methodology and complete results.
When to use it (and when not to)
Reach for all_observer when you want reactive state β counters, form fields,
loading flags, a reactive list, a computed summary β without adopting a full
architecture, and you want it composable with whatever DI/routing you already
use.
Reach for something else when you specifically need what it specializes in:
a compile-time-checked DI graph (Riverpod), an all-in-one framework with
routing and DI (GetX), or an auditable event/state architecture for a large
team (Bloc). all_observer has no opinion on where state lives, only on
how it notifies, so it composes with any of them.
Documentation
- Core concepts β
Observable,Observer, tracking,Computed. - Collections β
ObservableList/Map/Set. - Async β
ObservableFuture,ObservableStream,AsyncState. - Workers β
ever,once,debounce,interval. - Advanced β
batch, diamond dependencies,equals,setValue,strictMode, logging, design decisions, limitations. - Testing β how to test widgets and controllers that use all_observer, with real examples from the example app.
- Comparison β detailed comparison vs GetX, Riverpod, Bloc, MobX, signals.
- Migrating from GetX.
- FAQ β troubleshooting and common questions.
- Tutorials β four small examples: a toggle button, a loading screen, a login screen, an infinite list.
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:
| Package | Version | Description |
|---|---|---|
all_validations_br |
Brazilian document validation (CPF, CNPJ, CNH, PIX), input formatters/masks, JWT/UUID/currency/encryption utilities. | |
all_box |
Synchronous key-value storage with crash-safe writes and a pure-Flutter reactive layer. | |
all_image_compress |
Pure-Dart image compression (JPEG, PNG, GIF, BMP, TIFF, WebP), running in isolates. |
π₯ Contributors
Made with contrib.rocks.
How to contribute
Contributions are welcome! Read CONTRIBUTING.md to get started.
Issues and pull requests are welcome at the GitHub repository. Licensed under MIT.
Libraries
- all_observer
- Lightweight reactive state management for Flutter, with zero external
dependencies. Exposes Observable values, derived Computed values,
standalone reactive
effect()s, race-safe async state viaObservableFuture/AsyncState, the auto-tracking Observer widget (includingObserver.withChildfor static child subtrees), surgical per-widget rebuilds viawatch(context), reactive collections, manual subscriptions,Observable.batch(),Observable.select,untracked()/Observable.peek()escape hatches, workers (ever,once,debounce,interval), and scoped auto-cleanup viaReactiveScope/ScopedObserverMixin. - core
- Pure-Dart core of
all_observer: the dependency tracker, listener registry, batch/flush engine, typedefs, and observability primitives (ObserverInspector,RecordingInspector), plus theuntracked()escape hatch, theReactiveScope/ScopedObserverMixinscoped-cleanup primitives, and the error-reporting hook β with zero import ofpackage:flutter. Usable from a CLI/server context, not just Flutter apps. - engine
- The public, reusable reactive-graph engine of
all_observer(pure Dart, zero imports ofpackage:flutter).