all_observer 1.5.5 copy "all_observer: ^1.5.5" to clipboard
all_observer: ^1.5.5 copied to clipboard

Lightweight reactive state management for Flutter with zero external dependencies: observable values plus an auto-tracking Observer widget.

all_observer #

πŸ‡§πŸ‡· PortuguΓͺs | πŸ‡ΊπŸ‡Έ English

pub package license: MIT pub points 389 tests

Reactive state for Flutter with zero dependencies β€” final count = 0.obs; + Observer(...) and you're done.

all_observer hero

Table of contents #

Features #

  • πŸͺΆ Zero dependencies β€” the whole reactive core is built on Dart/Flutter alone, nothing else to keep in sync with your Flutter version.
  • βœ‚οΈ No boilerplate, no code generation β€” final count = 0.obs; plus Observer(() => ...) is a complete, working reactive pair.
  • 🎯 Granular rebuilds β€” dependencies are discovered by reading .value during 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 strictMode for CI).
  • πŸ§ͺ Testable by design β€” Observable/Computed are plain Dart objects, no wrapper/DI required to test them; see Testing.
  • πŸ”Œ ValueListenable interop β€” Observable<T> is a ValueListenable<T>, so it drops straight into ValueListenableBuilder, AnimatedBuilder, Listenable.merge.
  • 🩺 Built-in colored debug logging β€” flip ObserverConfig.logging = true and watch every create/update/track/dispose event in your terminal.

Installing #

flutter pub add all_observer
dependencies:
  all_observer: ^1.5.5
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');

More about Observable here.

Observer #

Auto-tracking widget: dependencies are re-discovered on every build, so conditional reads work out of the box.

Observer(() => Text('${count.value}'));

More about Observer here.

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

More about Computed here.

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

More about collections here.

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'),
));

More about async state here.

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

More about workers here.

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 big build(), 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.

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 pub Brazilian document validation (CPF, CNPJ, CNH, PIX), input formatters/masks, JWT/UUID/currency/encryption utilities.
all_box pub Synchronous key-value storage with crash-safe writes and a pure-Flutter reactive layer.
all_image_compress pub Pure-Dart image compression (JPEG, PNG, GIF, BMP, TIFF, WebP), running in isolates.

πŸ‘₯ Contributors #

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.

3
likes
0
points
1.12k
downloads

Publisher

verified publisheropensource.tatamemaster.com.br

Weekly Downloads

Lightweight reactive state management for Flutter with zero external dependencies: observable values plus an auto-tracking Observer widget.

Repository (GitHub)
View/report issues

Topics

#state-management #reactive #observable #reactive-programming

License

unknown (license)

Dependencies

flutter

More

Packages that depend on all_observer