all_observer 1.5.3
all_observer: ^1.5.3 copied to clipboard
Lightweight reactive state management for Flutter with zero external dependencies: observable values plus an auto-tracking Observer widget.
1.5.3 #
Patch release focused on async safety and regression hardening.
ObservableStream.run()now converts exceptions thrown synchronously bystreamFactoryintoAsyncError, matchingObservableFuture.- Failures returned asynchronously by
StreamSubscription.cancel()are isolated and reported throughCoreErrorReporting. - Added deterministic graph fuzzing, 20,000-cycle memory-retention stress, collection model tests, lifecycle/churn coverage, public-entrypoint smoke tests, and broad relative performance guards.
- Added a web release build of the example app to CI, including Flutter's Wasm compatibility dry run.
- Behavior change: synchronous exceptions from
streamFactoryno longer escape fromObservableStream.run(). Consumers that previously caught that exception withtry/catchmust now handle it through the emittedAsyncError. Public API signatures and reactive-engine behavior are unchanged.
1.5.2 #
- Added
ObsBool,ObsInt,ObsDouble, andObsStringas lightweighttypedefaliases for commonObservable<T>types. They preserve the completeObservableconstructor and behavior without adding classes. - Documented custom logging with
ObserverInspector, including multiple inspectors, external log sinks, optional stack traces, exception isolation, andRecordingInspectoraudit trails. - Added regression coverage for Observer lifecycle and subscription cleanup,
closed observables, collection notifications,
ReactiveScopedisposal,CoreComputeddependency cleanup, high-frequency updates, and custom inspectors. - No breaking changes.
1.5.1 #
ObservableList<E>gained factory constructors mirroringList's static equivalents:.filled,.empty,.from,.of,.generate,.unmodifiable. All accept an optionalnamefor the debug label. Internally they route through a new private_ownedconstructor that takes ownership of an already-fresh list instead of copying it again, so these factories don't pay for an extra allocation on top of the oneList.filled/.generate/etc. already did.ObservableList<E>gained convenience mutators:assign/assignAll(replace every element, notifying exactly once instead of once for an implicitclear()plus once for theadd/addAll),addIf/addAllIf(add only when aboolcondition istrue), andaddIfNotNull(add only when the value isn'tnull). All respect the same closed-collection no-op semantics as every other mutating member.- No breaking changes. Documented in
documentation/*/collections.md; covered by new tests intest/observable/collections/observable_list_test.dart.
1.5.0 — Engine v2 #
all_observer now runs on its own push-pull reactive graph engine, built
in-house from the ground up. It's the biggest change under the hood since
the package's first release — and it costs you nothing: no breaking
changes, no new dependencies, same public API. Every documented behavior
contract (glitch-free two-phase batching, in-batch read staleness, eager
per-flush settling, equals filtering, per-listener error isolation,
inspectors) is preserved, and the pre-existing 286-test suite passes
unmodified.
Why it matters: the new engine is O(1) on link/unlink, reuses links across
re-tracks with zero allocation when your dependency set doesn't change, and
propagates fully iteratively — so deeply nested reactive graphs are bounded
by available heap instead of the call stack. It also now ships as its own
public entry point, so you can build a custom reactive layer directly on
top of the same core engine that powers all_observer itself.
- New public entry point
package:all_observer/engine.dart: a standalone, pure-Dart, policy-free reactive graph engine (ReactiveEngine,ReactiveNode,ReactiveLink,ReactiveFlags) designed for third parties to build their own reactive layers on — the same machineryall_observeritself now runs on. Dependencies are intrusive doubly-linked lists (O(1) link/unlink, links reused in place across re-tracks — zero allocation when the read set doesn't change), node state is bit flags in a singleint(via a zero-costextension type), and both propagation phases are fully iterative (explicit stack), so graph depth is bounded by heap instead of the call stack. Behavior is delegated to three hooks:update(recompute; itsboolreturn is the propagation cut),notify(schedule a watcher) andunwatched(last-subscriber cleanup). Tutorial indocumentation/*/engine.md; a minimal, runnable signals implementation built on it lives intest/engine/fixtures/mini_preset.dart. CoreComputedrewired onto the engine (lib/src/core/engine_bridge .dart, exported bycore.dart): dependency tracking moved from per-recompute registry subscriptions (clear-all + resubscribe, one closure disposer per dependency per run) to engine links, and invalidation became push-pull — a flushed notification only marks dependents stale (ListenerRegistry.notifyAll→propagate), and an internalWatcherNode(created on first evaluation, kept untilclose()) pulls the fresh value in phase 2 of the sameBatchScopeflush as always. Reads landing between marking and settling resolve lazily on the spot (checkDirty), which also settles deep computed cascades within a single flush wave instead of one wave per graph level. Integration rides entirely onDependencyTracker.reportReadandListenerRegistry.notifyAll/notifyOrQueue, soCoreObservable,BatchScope, collections, async, workers and widgets are untouched. See ADR-0003 inARCHITECTURE.md.Observable.hasListenersnow also counts engine-graph dependents (aComputeddepending on the observable), preserving its long-standing meaning now that computeds are no longer plain registry listeners.- Self-dependency detection: a
Computedwhosecompute()reads its ownvalue(directly or through a chain) now throws a descriptiveObserverCycleErrorinstead of overflowing the call stack. TrackingContextgained asubscribesflag (defaulttrue, source-compatible): a recomputingCoreComputedpushes a non-subscribing context, keeping outer-context isolation, read counting andonTrackinspector events while the engine handles invalidation.- Engine test suites:
test/engine/(graph semantics: lazy pull, propagation cut via customequals, link reuse identity, conditional branch switching, 50k-deep chains without stack overflow, batching, automaticunwatchedcleanup) andtest/core/engine_bridge_test.dart(bridge semantics against the package contracts).
1.4.0 #
Additive release — no breaking changes, no new external dependencies.
- Surgical rebuilds with
watch(context):observable.watch(context)(andcomputed.watch(context)) subscribes the calling widget's ownElement—Observersemantics with no wrapper widget. Dependencies are re-discovered per build, multiple watched observables coalesce into one rebuild per batch/frame, rebuild scheduling shares the exact scheduler-phase handlingObserveruses (extracted into an internal helper), andonTrackinspector events fire with aWatch(<widget runtimeType>)label. Cleanup is lazy by design (Element has no third-party unmount hook): the first notification after unmount is a guaranteed no-op that releases every subscription of that element — documented indocumentation/*/advanced.md. In debug, callingwatchoutsidebuild()warns (throwsObserverErrorunderstrictMode); inside anObserver/Computed/effectit delegates the read to the active tracking context instead of double-subscribing. - Scoped cleanup with
ReactiveScope: an ambient, stack-based disposal scope in the pure-Dart core (exported by bothcore.dartandall_observer.dart). EveryComputed/CoreComputed,effect()and worker (ever/once/debounce/interval) created insidescope.run(...)registers its disposer in the scope;scope.dispose()tears everything down in LIFO order, idempotently, isolating disposer exceptions. Opt-in: creation outside a scope is unchanged. Nested scopes are disposed with their parent;add()on a disposed scope disposes the resource immediately with a debug warning (throws understrictMode). PlainObservables are deliberately not captured (no resource to release; documented). ScopedObserverMixin: pure-Dart controller counterpart ofObserverStateMixin—scoped(fn),autoDispose(disposer),disposeScope(),isScopeDisposed, and the underlyingscopeexposed.ObserverStateMixininternal refactor: now runs on an internalReactiveScope; public API unchanged, existing suite passes unmodified. Behavioral footnote: disposers now run in reverse registration (LIFO) order — previously registration order — and registering after dispose additionally dispatches a misuse warning (throws understrictMode), matchingReactiveScope.add.- Inspector: new
ObserverInspector.onScopeDispose(ScopeDisposeEvent)event (label +disposedCount), with a no-op default implementation like every other event;RecordingInspectorrecords it,ConsoleInspectorstays silent by default (as withonEffectRun). - Docs: new "Surgical rebuilds with
watch(context)" and "Scoped cleanup withReactiveScope" sections inadvanced.md(EN/PT-BR), updated comparison table (standalone effects, untracked reads, wrapper-less rebuilds, scoped cleanup),migration_from_getx.mdgained a "ReplacingonClosewithScopedObserverMixin" section, and both READMEs show a one-linewatch(context)example.
1.3.2 #
Documentation/example-only release — no code changes to lib/, no breaking
changes, no new external dependencies.
- Added real, runnable tests under
example/test/proving the library's testability: a widget test, a pure-Dart controller unit test, a measurable rebuild-granularity test, adebounceworker test usingflutter_test's virtual time, a deterministicObservableFuturetest via injected fakes, and astrictModemisuse test. - Extracted the example demos' business logic into constructor-injectable
controllers under
example/lib/controllers/(CounterController,FruitSearchController,FetchController) — the "testable architecture" pattern the new guide recommends. - New
documentation/en/testing.md/documentation/pt-BR/testing.mdguide, linking every snippet to the real test file it comes from; linked from both READMEs, both FAQs, and the doc navigation chain. - Added
example/README.mdand wiredexample/into CI (flutter testnow also runs there).
1.3.1 #
Documentation-only release — no code changes, no breaking changes, no new external dependencies.
- Restructured
README.md/README.pt-BR.mdinto a lean landing page, with the rest moved into topic guides underdocumentation/en/anddocumentation/pt-BR/(core concepts, collections, async, workers, advanced, comparison, migration from GetX, FAQ). - Added a hero image to both READMEs.
1.3.0 #
Additive release — no breaking changes, no new external dependencies.
- Core/Flutter split: the reactive engine (
CoreObservable,CoreComputed,DependencyTracker,ListenerRegistry,BatchScope) now has zeropackage:flutterimport and is exposed standalone viapackage:all_observer/core.dart.Observable/Computedare unchanged from the outside — they now wrap this engine internally. - Standalone reactivity:
effect(), plus escape hatchesuntracked(),Observable.peek(), andObservable.previousValue. - Pluggable observability: the
ObserverInspectorinterface (onCreate/onUpdate/onDispose/onTrack/onWarning/onEffectRun), with the classic colored console output now a formalConsoleInspectorimplementation, plus aRecordingInspectorandObserverConfig.inspectors/captureStackTraces. - Async:
ObservableStream<T>, theStreamcounterpart ofObservableFuture, with the same generation-counter race safety.AsyncValue<T>added as an alias forAsyncState<T>. - Lifecycle helpers:
ObserverStateMixin(auto-disposedeffect()s and subscriptions on aState),ObservableStore/Observable.persistWith(optional persistence integration point, e.g. forall_box), andObservableHistory/Observable.withHistory(bounded undo/redo). - Docs:
ARCHITECTURE.mdexpanded to cover all of the above,CONTRIBUTING.mdadded, CI workflow added, and both READMEs gained a comparison section against GetX/Riverpod/MobX/flutter_hooks.
Tests: +57 new tests; total 225.
1.2.0 #
Minor release — no breaking changes, no new external dependencies.
Core:
- T2.1 — Auto-batch (glitch-free by default): every write — even a
standalone
observable.value = xoutside any explicitObservable.batch()— is now automatically routed through the same two-phase fixed-point flush that explicit batches use. Diamond dependency graphs (ComputedA and B both derived from source S,ComputedC depending on A and B) always recompute exactly once, always seeing fully settled upstream values — no glitch, no explicitbatch()required. The implementation wraps each standalone write in a micro-batch (BatchScope.run(() => BatchScope.queue(this))); a fast-path skips the overhead entirely when there are no listeners. ThekMaxFlushWaves = 100guard from v1.1.1 (T1.1) is the safety net for any in-batch cycle that could result from cascading writes. Observable.batch()repositioned as a performance optimization: wrapping multiple writes inbatch()coalesces them into a single notification round (all writes committed first, then each changed observable notifies once), instead of opening a micro-batch per write. Usebatch()whenever you write to more than one observable in the same logical action.- "Known limitations" section updated in both READMEs: removed "Diamond
glitch outside batch" and "Deeper cross-branch cascades" items; replaced
with a paragraph explaining that
batch()is now a performance tool, not a consistency requirement.
Tests: +5 new tests (T2.1 auto-batch group: diamond without batch, 3-level cascade, write-in-ever-callback, explicit-batch coalescing, post-close no-op); total 168.
1.1.1 #
Patch release — no breaking changes, no new external dependencies.
Bug fixes (audit):
- T1.1 — Flush wave limit (CRITICAL):
_flushPendinginbatch_scope.dartnow has a bounded iteration limit (kMaxFlushWaves = 100). A mutual cyclea.listen((v)=>b.value=v+1)+b.listen((v)=>a.value=v+1)inside abatch()previously caused an infinite loop becausekMaxNotificationDepthonly guards nested call-stack recursion, not iterativewhilewaves. The new guard detects the wave overflow, clears pending queues, and reports a descriptive bilingualFlutterErrorinstead of hanging indefinitely. - T1.2 —
previousDatasurvives chainedrun()calls: callingObservableFuture.run()a second time before the first completes previously silently erased the stale value (.valueOrNullreturnednullwhen state was alreadyAsyncLoading). Fixed via_previousDataFromCurrent()helper:AsyncData→ preserve its value;AsyncLoading→ propagate its ownpreviousData;AsyncError→null(documented deliberate choice).
Documentation:
- T1.3 —
Observable.refresh()polymorphic semantics: new bilingual paragraph documenting that subclasses may extendrefresh()beyond a simple notification (e.g.ObservableFuture.refreshre-runs the fetch). No code change.
Tests: +7 new tests (4 for T1.1 wave-limit scenarios, 3 for T1.2 chained
run() cases); total 163.
1.1.0 #
Additive release — no breaking changes, no new external dependencies.
Async:
ObservableFuture<T>: anObservable<AsyncState<T>>that runs aFuture<T> Function()and tracks its loading/data/error lifecycle. Auto-runs by default (autoStart: true) or viarun()/refresh(). Race-safe: a generation counter discards a stalerun()'s result if a newerrun()started before it resolved, and discards any result that arrives afterclose().AsyncState<T>sealed class:AsyncLoading<T>(withpreviousDatafor stale-while-loading UIs),AsyncData<T>,AsyncError<T>;when/maybeWhen,isLoading/hasData/hasError/valueOrNull, content-based==/hashCode.
Computed:
equalsnamed parameter onComputed's constructor, mirroringObservable's, for custom change-detection (e.g. floating-point tolerance) on the recomputed value.- Diamond-glitch mitigation: inside an active
Observable.batch(), aComputed's recompute is deferred (to the nextvalueread, or to end-of-batch, whichever comes first) instead of running eagerly per upstream notification — so a diamond-shaped dependency graph recomputes at most once per batch, with every dependency already at its final value. Outsidebatch, the previous eager-recompute-per-notification behavior is unchanged and documented as a known limitation.
Widgets:
Observer.withChildnamed constructor: rebuilds only the part of the subtree the builder itself constructs, reusing a staticchildwidget across rebuilds instead of reconstructing it — for expensive widgets that don't depend on any observable.
Observable:
Observable.setValue(T newValue): equivalent tovalue = newValue, usable as a tear-off (e.g. directly as anonChangedcallback) and the unambiguous way to assignnullto anObservable<T?>(unlikecall(null), which reads instead of assigns).Observable<T>.select<R>(R Function(T value) selector, {String? name})extension: sugar overComputed(() => selector(value), name: name)for a narrower, memoized derived value. Caller owns and mustclose()the returnedComputed.
Performance:
ListenerRegistrynow backed by aLinkedHashSet<VoidCallback>instead of aList, makingadd/remove/containsO(1) instead of O(n), while preserving insertion order and native deduplication. Seebenchmark/listener_registry_benchmark.dart.
Other:
example/reworked into a multi-page app (bottom navigation) with five focused demos: counter +Computed, debounced search,ObservableFutureasync states,Observable.batchform saving, andObserver/ValueListenableBuilderinterop — each in its own file underexample/lib/demos/.- README (EN/PT-BR): new "Migrating from other reactive state solutions" section (concept-based equivalence table) and a "Known limitations" section (diamond glitch outside
batch,Computedstaying active after first read untilclose(), single-isolate confinement).
1.0.0 #
Initial release.
Core:
Observable<T>reactive value, plusObservableInt,ObservableDouble,ObservableBool,ObservableString..obsextension for creating observables from literals and collections.ObservableList,ObservableMap,ObservableSetreactive collections; reading any member tracks it, mutating any member notifies at most once per call — bulk operations (addAll,removeWhere,retainWhere,insertAll,clear,sort,shuffle) never notify once per element, and no-op mutations (adding a duplicateSetelement,removeWherethat removes nothing,map[k]=vwith an identical value,clear()on an already-empty collection) don't notify at all.Observerwidget with automatic, stack-based dependency tracking and safe rebuild scheduling (no "defunct element" crashes).ObserverValuefor self-contained local reactive state.Computed<T>: lazy, memoized derived values reusing the same dependency tracker asObserver; supports dynamic/conditional dependencies; only notifies when the derived value actually changes;close()unsubscribes from all current dependencies.Observable.batch(() { ... }): coalesces multiple writes inside the callback into a single notification per changed observable/collection for manual (listen/ever) subscribers, with nested-batch support.equalsparameter on theObservable<T>constructor, for custom change-detection (e.g. floating-point tolerance) instead of==.listen/ObservableSubscription, stream-free manual subscriptions.- Workers:
ever,once,debounce,interval, plusWorker/Workers.
Robustness:
- An exception thrown inside one
ever/listencallback does not stop other listeners of the same observable from running; it is reported viaFlutterError.reportError(library: 'all_observer') instead of being swallowed or propagating. - A synchronous update cycle (a listener of A writing to B, whose listener writes back to A) stops after a bounded notification depth with a descriptive
FlutterError, instead of crashing with a raw stack overflow. Observer: if the builder throws during a tracked build, dependency disposers accumulated up to that point are still assigned, so the next build/unmount does not leak or double-register listeners.- The "write during build" warning covers both
Observable.value =and reactive-collection mutations, andObserverConfig.strictModeturns this case into a thrownObserverErroras well as the "empty Observer" case. listen()/close()on an already-close()dObservable/collection returns an inert (already-canceled) subscription instead of silently registering a listener that can never fire; a mutation attempted on a closed collection is a full no-op (the underlying data is left untouched), and double-close()is safe.- Documented, in
Observable's dartdoc, that writing to an observable from a different isolate does not work (no cross-isolate synchronization is attempted).
Other:
- Debug-only, ANSI-colored logging via
ObserverConfigand non-fatal misuse warnings (with optionalstrictMode). - Zero external dependencies; full
ValueListenableinteroperability.
See AUDIT_REPORT.md at the repository root for the full item-by-item performance/robustness audit this release was validated against.