🇧🇷 Português | 🇺🇸 English
Reactive state for Flutter with zero dependencies — final count = 0.obs; +
Observer(...) and you're done.
- 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
- 🪶 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.
flutter pub add all_observer
dependencies:
all_observer: ^1.5.6import 'package:all_observer/all_observer.dart';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++;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());
}final count = 0.obs; // ObservableInt.obs wraps any value in an Observable — count now holds 0 and can be watched for changes.
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()).
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.
ObserverConfig.logging = true;[all_observer] ✚ Observable<int>(count) created → 0
[all_observer] 👁 Observer(unnamed) tracking: [count]
[all_observer] ↻ Observable<int>(count): 0 → 1
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');Auto-tracking widget: dependencies are re-discovered on every build, so conditional reads work out of the box.
Observer(() => Text('${count.value}'));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.
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 neededObservableList/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 mutationRuns 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'),
));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));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.
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.
| 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 |
// 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 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. |
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.
| 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.
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.
- 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.
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. |
Made with contrib.rocks.
Contributions are welcome! Read CONTRIBUTING.md to get started.
Issues and pull requests are welcome at the GitHub repository. Licensed under MIT.
