zenify 2.2.0
zenify: ^2.2.0 copied to clipboard
Complete state management for Flutter — hierarchical dependency injection, intelligent async caching, and offline-first resilience. Zero boilerplate, automatic cleanup.
2.2.0 #
Added #
- Global Query & Mutation Hooks (
Zen.isFetching,Zen.isMutating): Added top-level reactive getters and counters toZenfor effortless global status indicators:Zen.isFetching&Zen.activeFetches(RxInt): Reactive hooks to detect in-flight queries across the app (ideal for top-level progress bars or app bar spinners).Zen.isMutating&Zen.activeMutations(RxInt): Reactive hooks to detect in-flight mutations across the app (ideal for disabling action buttons or showing save overlays).ZenMutation.isMutatingKey(key)&ZenMutation.activeMutationsForKey(key): Query active in-flight mutation status for specific mutation keys.ZenQueryCache.isFetching(...)&ZenQueryCache.isFetchingCount(...): Filter active query fetch status byqueryKey,tag, or custompredicate.
- Memory-Bounded Infinite Queries (
ZenInfiniteQuery.maxPages): AddedmaxPagesparameter toZenInfiniteQueryto prevent unbounded memory growth during long scroll sessions. Automatically evicts oldest pages from the head during forward pagination or newest pages from the tail during backward pagination, while maintaining cursor continuity for seamless bidirectional refetching.
2.1.1 #
Fixed #
- Sequential Offline Mutation Replay: Enforced strict FIFO ordering and stop-on-failure execution during offline mutation queue replay in
ZenMutationQueue. Prevents out-of-order execution in order-sensitive workloads (such as chat sequences and transactions) when network connectivity drops or errors occur. ZenQueryConsumerInstance Sharing: Introduced reference-counted query instance resolution inZenQueryConsumer. Multiple consumer widgets mounted with the exact samequeryKeynow share a single activeZenQueryinstance, completely eliminating duplicate queries and transient dual in-flight fetches.- Cancellable Retry Delays: Implemented
Completer-based delay cancellation inZenQueryandZenMutation. Disposing or resetting a query/mutation during an active backoff window cancels the delay immediately.dispose()halts retrying and frees resources;reset()halts retrying and leaves the mutation in idle state, fully reusable. ZenMutationQueueSubscription Leak:setNetworkStreamnow stores and cancels the previousStreamSubscriptionbefore subscribing to a new stream. CallingsetNetworkStreammore than once (e.g. after a hot-restart or test re-init) no longer accumulates duplicate connectivity listeners.
2.1.0 #
Added #
keepPreviousDataparameter added toZenQueryConsumerto prevent loading flashes during pagination.ZenQueryCache.prefetchnow returns aFuture<T?>allowing awaiting of prefetch results.
Fixed #
- Stack Overflow Fix: Replaced recursive retries in
ZenMutationandZenQuerywith iterativewhileloops to prevent stack overflow errors under high retry counts or spotty networks. - Retry Delay Reliability:
_calculateRetryDelaynow correctly requires a non-nullable error parameter, eliminating a bug whereretryDelayFncould receive the wrong exception fallback. - State Cleanups: Removed orphaned internal state (e.g.
_retryAttemptfields) for safer concurrent fetches.
2.0.0 — V2 Architecture #
This release completes the architectural shift to fully tree-bound dependency injection. Every controller access is now resolved via BuildContext — the same model Flutter uses for Theme, MediaQuery, and Navigator. The result is multi-instance safety by construction, perfect test isolation with zero global teardown, and a mental model that transfers directly to how Flutter works.
Migration: See migration_v2_0_0.md for the full guide.
The mechanical change is adding acontrollerparameter to everyZenView.build()override.
Breaking Changes #
Architecture & lifecycle
ZenRouteObserverremoved. The V1NavigatorObserver-based controller disposal has been removed. UseZenRouteinstead — it disposes its scope automatically when the widget leaves the tree, with zero manual registration required.ZenController.autoDisposeandlimitedworkers removed. These were trivially composable usingever()andonce().getResourceStats()['memory_overhead_estimate']removed. The hardcoded estimate was misleading without a real heap profiler.ZenServicenow extendsZenController. The standalone duplicate lifecycle implementation has been replaced.ZenServiceis nowabstract class ZenService extends ZenController {}. Consumer code (class MyService extends ZenService) is unchanged. Removed:ZenService.disposeAllServices(),ZenService.activeServiceCount,ZenService.getActiveServiceTypes(),ZenService._activeServices,ensureInitialized(). Services now benefit from fullRxtracking and worker auto-disposal via theZenControllerbase. UseZen.reset()in tests instead ofZenService.disposeAllServices().
The Three-Step Mental Model #
REGISTER → Zen.registerModules([AppModule()]) app services — live for app lifetime
ZenRoute(moduleBuilder: () => M()) route-level — standard for real apps
↳ scope.put<T>(T()) inside ZenModule where controllers are actually created
ZenProvider.create<T>(create: ...) simple routes without a module
CONSUME → ZenView<T> extend — controller injected into build()
ZenConsumer<T> compose — inline builder, no inheritance
context.controller<T>() imperative — from any widget or callback
REACT → ZenObserver Rx<T> — auto-rebuild on value change
ZenUpdater<T> update() — manual, ID-targeted rebuilds
ZenQuery<T> async — caching, loading, error, refetch
Key rule:
ZenViewresolves from the nearest scope automatically. It doesn't matter whether the controller was registered viaZenRoute,ZenProvider, orZen.put— consumption is always the same zero-boilerplate pattern.
Global API cleanup
| Removed | Replacement |
|---|---|
Zen.currentScope / setCurrentScope / resetCurrentScope |
context.zenScope (widget tree) or Zen.rootScope |
Zen.get<T>() |
Zen.find<T>() |
Zen.has<T>() |
Zen.exists<T>() |
Zen.remove<T>() |
Zen.delete<T>() |
Obx widget |
ZenObserver |
Ref<T> is removed entirely. It was a thin wrapper over scope.find() with no additional value.
ZenWorkerHandle renamed to ZenWorker
The handle returned by all ZenWorkers.* factory methods and ZenController.* worker methods is now named ZenWorker. Update any type annotations in your code:
// ❌ V1-style name (removed)
ZenWorkerHandle handle = ZenWorkers.ever(count, (_) => update());
// ✅ V2
ZenWorker worker = ZenWorkers.ever(count, (_) => update());
ZenWorkerGroup is unchanged.
ZenDependencyAnalyzer — REMOVED
ZenDependencyAnalyzer is removed. It contained only stubbed, non-functional implementations. There is no replacement.
ZenConfig — two flags removed
| Removed field | Reason |
|---|---|
checkForCircularDependencies |
No runtime implementation existed — the flag had no effect |
enableDependencyVisualization |
No runtime implementation existed — the flag had no effect |
The corresponding configure() parameters circularDependencyCheck: and dependencyVisualization: are also removed.
ZenMetrics — instrumentation methods and fields removed
The following methods were never called by the library itself (the counters they wrote to were always zero):
| Removed method | Removed field |
|---|---|
recordRxCreation() |
totalRxValues |
recordStateUpdate() |
totalStateUpdates |
recordProviderCreation() |
totalProviders |
recordEffectSuccess() |
totalEffectRuns, totalEffectSuccesses, effectSuccessCounts |
recordEffectFailure() |
totalEffectFailures, effectFailureCounts |
getReport() no longer includes a state or effects block (both were always zeros). The remaining API — incrementCounter, recordCounterValue, startTiming, stopTiming, getAverageDuration, getReport, startPeriodicLogging, stopPeriodicLogging, reset — is unchanged.
ZenRoute — scope resolution bug fixed
ZenRoute no longer mutates a global Zen.currentScope pointer on every push. Scope resolution is now:
- Explicit
parentScope:parameter - Nearest
ZenProviderancestor in the widget tree (context.zenScope) Zen.rootScope— stable fallback for top-level routes
Sibling routes pushed via Navigator.push no longer silently inherit each other's scopes.
ZenView.build() — explicit controller parameter (required)
V1 provided the controller via a magic controller getter backed by a global static registry. V2 injects it as an explicit parameter — resolved from the nearest ZenProvider ancestor by the framework.
// ❌ V1 — magic getter, global registry
class CartPage extends ZenView<CartController> {
@override
Widget build(BuildContext context) {
return Text('${controller.totalItems}');
}
}
// ✅ V2 — explicit injection, compiler-enforced
class CartPage extends ZenView<CartController> {
const CartPage({super.key});
@override
Widget build(BuildContext context, CartController controller) {
return Text('${controller.totalItems}');
}
}
The migration is mechanical: add , CartController controller to every ZenView.build() override.
ZenScopeWidget renamed to ZenProvider
The canonical provider widget is now ZenProvider. All usages of ZenScopeWidget must be renamed.
// ❌ V1
ZenScopeWidget.create<CartController>(create: () => CartController(), child: ...)
// ✅ V2
ZenProvider.create<CartController>(create: () => CartController(), child: ...)
ZenControllerScope — REMOVED (not deprecated)
ZenControllerScope conflated widget lifecycle with DI lifecycle — two concerns that must be kept separate. It is removed entirely in V2. Replace with ZenProvider.create:
// ❌ V1 — removed
ZenControllerScope<MyController>(
create: () => MyController(),
child: MyView(),
)
// ✅ V2
ZenProvider.create<MyController>(
create: () => MyController(),
child: const MyView(),
)
initController on ZenView — REMOVED
Per-instance self-owned controller via initController is removed. It tightly coupled widget and DI concerns. Use ZenProvider.create at the callsite instead:
// ❌ V1 — initController override (removed)
class ChatBubble extends ZenView<ChatBubbleController> {
final String messageId;
@override
ChatBubbleController Function() get initController =>
() => ChatBubbleController(messageId: messageId);
...
}
// ✅ V2 — provide at callsite
ZenProvider.create<ChatBubbleController>(
create: () => ChatBubbleController(messageId: messageId),
child: const ChatBubble(),
)
_ZenViewRegistry — deleted
The global Map<Type, List<ZenController>> static registry is gone entirely. There is nothing to migrate.
ZenView.scope property — removed
Scope association is done exclusively via ZenProvider in the widget tree.
context.controller<T>() — no global fallback
The global Zen.findOrNull<T>() fallback has been removed from context.controller<T>(). Resolution is strictly tree-bound. If no ZenProvider ancestor provides the type, ZenControllerNotFoundException is thrown with an actionable message.
Controller Resolution (V2) #
ZenView resolves its controller in this order:
- Nearest
ZenProviderancestor — O(1)InheritedWidgetlookup. ZenControllerNotFoundException— thrown with a clear, actionable message if not found.
There is no global fallback. This is intentional — silent fallbacks to global state hide architectural mistakes.
New #
ZenProvider.create<T> — single-controller shorthand
The most common pattern (one controller per route) now has a dedicated constructor:
ZenProvider.create<CartController>(
create: () => CartController(),
child: const CartPage(),
)
ZenUpdater<T> — renamed from ZenBuilder<T>
ZenBuilder<T> is now ZenUpdater<T>. The name communicates the purpose: this widget rebuilds when controller.update() is called. ZenBuilder is fully removed — any remaining usage is a compile error.
ZenConsumer<T> — fixed and production-ready
ZenConsumer previously used global DI only. V2 fixes it to resolve via context.controller<T>() in didChangeDependencies() — proper InheritedWidget subscription semantics, scope-bound, fails fast.
Performance #
- No global registry — resolution is O(1) via Flutter's native
InheritedWidgethash map. - Fewer allocations —
ZenViewusesComponentElementinstead ofStatefulWidget. Each view allocates 2 objects (Widget + Element) instead of 3 (Widget + Element + State). ZenUpdaterlistener churn fixed —didChangeDependencies()uses anidentical()guard to avoid re-attaching listeners when the resolved controller hasn't changed.
Bug Fixes #
ZenUpdatersilent failures — now stores resolution errors and rethrows inbuild(), routing toonErrorif provided.ZenProviderasync init — module init errors now surface viaFlutterError.reportError()instead of being silently logged.ZenConsumerfail-fast — throws clearly when controller not found instead of silently degrading.
Tests #
2,082 tests, all passing. Test suite fully migrated to V2 patterns — no global Zen.put for UI controllers in any widget test.
Migration Checklist #
# Find all ZenView.build() overrides that need updating:
grep -rn "Widget build(BuildContext context)" lib/ --include="*.dart"
- Add
, T controllerparameter to everyZenView.build()override - Replace
ZenScopeWidget→ZenProvider(andZenScopeWidget.create→ZenProvider.create) - Replace
ZenControllerScope→ZenProvider.create(removed, must migrate) - Remove
initControlleroverrides → move controller creation toZenProvider.createat callsite - Replace
ZenBuilder→ZenUpdater(required —ZenBuilderis fully removed, will not compile) dart analyze→ confirm 0 errorsflutter test→ confirm all tests pass
Full migration guide →
1.11.0 #
Fixed #
-
ZenViewmulti-instance registry — replaced the flatMap<Type, ZenController>registry with a stack-based registry (Map<Type, List<ZenController>>). EachZenViewinstance now pushes its controller on mount and pops on dispose. Thecontrollergetter usespeek()to always return the innermost (most recently mounted) instance — analogous to Flutter's ownInheritedWidgetnearest-ancestor semantics. This fixes state-bleed when navigating between multiple instances of the same page type (e.g., navigating toCartPagetwice in the back stack). -
ZenRoutescope unification —ZenRoutepreviously maintained its own private_ZenScopeProviderInheritedWidgetalongside the canonical one inZenScopeWidget. These are now unified:ZenRouteuses the sharedZenScopeProviderandcontext.zenScopeextension fromzen_scope_widget.dart. Eliminates a source of duplicate scope discovery logic. -
ZenBuilderimport — fixed import inzen_builder.dartwhich was incorrectly depending onzen_route.dart's private scope provider rather than the canonicalzen_scope_widget.dart. -
context.controller<T>()resolution — theZenViewContextExtensions.controller<T>()method previously had a redundant step that looked up the internal_ZenViewRegistry. Removed. Resolution is now purely widget-tree-bound: nearestZenScope→ globalZen.findOrNull<T>(). This makescontext.controller<T>()100% multi-instance safe with no global registry involved.
Improved #
-
ZenScopeProvider— moved from public export to implementation-internal. It was always an implementation detail (theInheritedWidgetbackingZenScope) and was never documented for direct use. Users who need it can still access it via a direct import but it is no longer part of the default package surface. -
doc/v2_architecture_design.md— added architectural design document capturing the rationale for current V1.x fixes and the full V2 breaking-change roadmap (ZenViewinjected build,ZenConsumer<T>,ZenScopeWidget-as-canonical-provider). Serves as the reference for V2 development.
1.10.4 #
Added #
RxList.assignAll()— convenience method to replace all elements in a reactive list in a single call. Also addstryAssignAll()for explicit error-handling.
Improved #
ZenStoragedocumentation — clarified thatwrite()/read()pass a plainMap<String, dynamic>, not a JSON-encoded string. Added a copy-paste Drift/SQLite adapter recipe alongside the existing SharedPreferences example.ZenQueryConfig.fromJson/toJsondocumentation — clarified that these work withMap<String, dynamic>directly, making Drift'stoMap()/fromMap()plug in without any extra JSON encoding step.
1.10.3 #
Added #
-
ZenQueryConfig.retryWhenOnline— network-aware retry pausing. Whentrue, a query that exhausts all its retry attempts while the device is offline (or goes offline mid-retry) enterspaused(fetchStatus = ZenQueryFetchStatus.paused) instead of the usualerrorstate. It is automatically registered in an internal retry queue and restarts a fresh, full retry cycle (with exponential backoff) the moment connectivity returns — no manualinvalidate()orrefetch()required.// Query silently waits for connectivity and self-heals on reconnect final query = ZenQuery<User>( queryKey: 'user:me', fetcher: (_) => api.getMe(), config: ZenQueryConfig( retryCount: 3, retryWhenOnline: true, // NEW ), );Key behaviours:
- Stale cache preserved: if the query already has data, it returns the cached value while paused.
- Multiple queries: all queued queries are drained atomically when connectivity returns.
- Disabled queries: skipped at drain time if
enabled = false. - Dispose safety: disposed queries are automatically cleaned up from the queue.
- Off by default:
retryWhenOnline: falsepreserves the existing error-on-exhaustion behaviour.
1.10.2 #
Added #
ZenInfiniteQuery.maxPages— memory safety limit for infinite scrolling. Automatically evicts older pages from RAM when users scroll deeply, while preserving the exact cursors needed for seamless bi-directional refetching.ZenMutation.anyMutating&ZenMutation.activeMutations— global tracking hooks. Perfect for showing universal loading indicators or disabling navigation while mutations are actively in-flight anywhere in the app.
1.10.1 #
Added #
-
scope.require<T>({String? tag})— idiomatic throwing dependency lookup onZenScope. This is the one genuinely new addition.scope.find<T>()is nullable (returnsT?), which meansscope.find<T>()!is a silent footgun.scope.require<T>()is the non-nullable, throwing form that surfaces aZenDependencyNotFoundExceptionwith the missing type name, scope name, tag, and a copy-paste registration suggestion.// ❌ Before — NPE crash, no diagnostic context final svc = scope.find<AuthService>()!; // ✅ After — ZenDependencyNotFoundException with actionable message final svc = scope.require<AuthService>();Note:
Zen.find<T>()(global API) already throws when missing — it behaves likeGet.find<T>(). The newrequire<T>()is only onZenScopewherefindis nullable. -
ZenQueryConsumer<T>— self-contained widget that creates, fetches, and renders aZenQuerywith no controller or module boilerplate. TheuseQueryequivalent for Zenify.// No controller. No module. One widget. ZenQueryConsumer<User>( queryKey: 'user:123', fetcher: (_) => api.getUser(123), data: (user) => UserProfile(user), loading: () => const CircularProgressIndicator(), error: (err, retry) => ErrorView(err, onRetry: retry), )Supports all
ZenQueryBuilderfeatures:initialData,config,showStaleData,autoFetch, and dynamicqueryKeychanges (disposes the old query, creates a new one).
Docs Updated #
doc/hierarchical_scopes_guide.md—ZenModule.registerexamples updated toscope.require<T>().doc/real_world_patterns.md—NetworkModuleandAuthModuleexamples updated toscope.require<T>().doc/migration_guide.md— DI section clarifies thatZen.find<T>()already throws likeGet.find<T>(), and introducesfindOrNull<T>()as the explicit nullable escape hatch.
Tests #
test/di/zen_require_test.dart— 21 tests:scope.require<T>()happy path (untagged, tagged, hierarchical, lazy), error path (exception type, type name, scope name, tag, suggestion), parity withfindRequired<T>(), disposed scope, andZen.find<T>()throwing behaviour.test/widgets/zen_query_consumer_test.dart— 14 widget tests: happy path,initialData, loading state (custom + default), error state (custom + default + retry), idle state,queryKeychange, disposal, config passthrough, typed generics, and barrel export smoke test.
1.10.0 #
Added #
ZenInfiniteQuery.when()— declarative builder for infinite-scroll UI. Handles all states (loading, error, data, loadingMore, empty) with optionalfooterslot for a "load more" trigger.ZenMutation.when()— reactive mutation state builder usingAnimatedBuilder+Listenable.merge. Covers idle, loading, success, and error states with full type parameters.doc/gorouter_guide.md— comprehensive guide for integratingZenRoutewith GoRouter, covering nested routes, auth guards, and scope lifecycle.- Codecov badge in README.
Bug Fixes #
These bugs were uncovered through the new test suite ("coverage-driven debugging"). All are fixed in this release.
RxTransformations.skip()never updated after creation — the closure returnednullwithout accessingvalue, so the sourceRxwas never registered as a dependency. The computed was permanently dead and would never re-evaluate when the source changed. Fixed by always accessingvaluebefore branching.RxTransformations.take()lost its dependency after exhaustion — same root cause. Fixed for consistency.RxTimingExtensions.take/skipcaused API-wide ambiguity —RxTimingExtensionsdefined callback-styletake(int, callback)andskip(int, callback)onRx<T>, conflicting withRxTransformations.take(int)and.skip(int). Dart could not resolve either name. Renamed totakeFirst/skipFirst— consistent with Flutter'sfirstWhere/firstOrNullpattern.RxTimingExtensions.map/wherecaused API-wide ambiguity —RxTimingExtensionsdefined callback-stylemap(transformer, callback)andwhere(condition, callback)onRx<T>, conflicting withRxComputedExtensions.map/.wherewhich both return computed values. Renamed tolistenMapped(transformer, callback)andlistenWhere(condition, callback)— thelisten*prefix makes the side-effect callback intent explicit and unambiguous.RxMapExtensions.update()was unreachable dead code —Rx<T>has a class-levelupdate(T Function(T) updater)that always shadows the extension methodupdate(K key, V Function(V), ...). Users could never call the map-keyed version. Removed; usetryUpdate(key, fn)directly.RxTransformations.whereNotNull()caused ambiguity for nullable types — conflicted withRxNullableTransformations.whereNotNull(). Removed fromRxTransformations<T>;RxNullableTransformations<T>.whereNotNull()is the correct location.ZenTestMode.mockAll()silently registers underdynamic(breaking change documented) — callingZen.put(instance)whereinstanceis typed asdynamicloses the generic type parameterTto type erasure. All mocks were registered underdynamicinstead of the intended interface type, makingZen.find<T>()always returnnullregardless of what was mocked. Deprecated with a clear runtime warning explaining the bug. Use chained.mock<T>()calls instead.ZenModuleRegistrydead code removed — the missing-dependency guard inregisterModuleswas unreachable because_collectAllDependencies()already recursively resolves all transitive dependencies before the check was reached. Removed to prevent misleading error messages.ZenController.autoDispose<T>()did not dispose handle on error — if the callback threw an exception, the handle was orphaned. It now callshandle.dispose()in the catch block, consistent with the no-leak guarantee.ZenController.limited<T>()did not dispose handle on error — same issue. Now disposes on callback exception.
Tests #
- Comprehensive Test Coverage: Over 2,120+ unit and widget tests validating core logic, widget interactions, garbage collection, and strict state invariants. Line test coverage has reached >95.0% overall!
Big Fixes & Minor Tweaks #
- Fixed Lifecycle Hook Warnings: Resolved
mustCallSuperlint warnings within ZenController'sonInitandonReadyhooks, ensuring developers properly chain initialization methods. - Widget Testing Stability: Fixed pending timer exceptions during test disposal by ensuring immediate flush of garbage collection timers within Widget tests affecting
ZenQueryBuilder,ZenInfiniteQuery.when(), and cache operations.
Dead Code Audit #
As part of the final push for test coverage, we removed/ignored non-reachable safety pathways:
- Scope clearAll fallback:
// coverage:ignore-lineadded to the error catch insideclearAllsincedisposecompletely swallows exceptions within ZenScope's controller array cleanup phase. - ControllerScope key update: Standard Flutter validation (
Widget.canUpdateverifies identical keys) effectively prevents instances with different keys from executingdidUpdateWidget. Thus, key differences trigger complete teardowns via widget replacement rather than widget updates. This redundant internal key check insideZenControllerScope.didUpdateWidgethas been marked with// coverage:ignore.
1.9.1 #
Added #
ZenQuery.when()— declarative shorthand forZenQueryBuilder. Supportsdata,loading,error, andidlebuilders.tool/migrate_from_getx.dart— migration script that auto-converts GetX code to Zenify (imports, controllers,.obs→.obs(), DI calls, widgets,permanent:→isPermanent:). Flags navigation, workers, and other patterns for manual review.- GitHub Actions CI workflow (
flutter test,dart analyze, format check) - GetX migration guide (
doc/migration_guide.md) complete rewrite with verified API examples - README callout for GetX developers linking to the migration guide
1.9.0 #
Added #
tagsparameter onZenQueryfor group-based cache managementZenQueryCache.invalidateQueriesByTag(tag)andrefetchQueriesByTag(tag)ZenQueryCache.invalidateQueriesByPattern(pattern)andrefetchQueriesByPattern(pattern)— supports glob-style*wildcardsZenQueryCache.getQueriesByTag(tag)andgetKeysByTag(tag)for tag inspectionInMemoryStorage— a built-in zero-dependencyZenStorageadapter for testing and ephemeral storage- Warning log when a wildcard pattern matches all queries (e.g.
'*')
1.8.0 #
Breaking Changes #
- Removed
ZenInspectorOverlaywidget. Remove the wrapper from your app —Zen.init(registerDevTools: true)continues to work as before.
// Before
runApp(ZenInspectorOverlay(child: MyApp()));
// After
runApp(MyApp()); // DevTools integration unchanged
Changed #
- DevTools UI moved to a separate optional package:
zenify_devtools_extension - Core package is ~25MB smaller as a result
1.7.0 #
Added #
- Flutter DevTools extension with 3-tab inspector: Scope Inspector, Query Cache Viewer, Metrics Dashboard
ZenServiceExtensionsclass for VM Service Protocol integration- Automatic service extension registration via
Zen.init(registerDevTools: true)in debug builds - Zero performance impact in release builds
1.6.6 #
Added #
ZenExceptionbase class with compact (default) and verbose (ZenConfig.verboseErrors = true) error formatting- Specific exception types:
ZenDependencyNotFoundException,ZenCircularDependencyException,ZenDisposedScopeException,ZenScopeNotFoundException,ZenControllerNotFoundException,ZenControllerDisposedException,ZenOfflineException,ZenQueryException,ZenMutationException,ZenModuleException,ZenLifecycleException,ZenRouteException ZenLogger.logException(e)for formatted exception outputZenConfig.verboseErrorstoggle
1.6.5 #
Added #
- "Pending Mutations" card in DevTools Stats View
ZenMutationQueue.pendingCountandpendingJobsfor debugging- Visual offline queue status indicator in DevTools
1.6.4 #
Added #
ZenObserver— alias forObxwidgetscope.get<T>()— alias forscope.find<T>()scope.remove<T>()— alias forscope.delete<T>()scope.has<T>()— alias forscope.exists<T>()
1.6.3 #
Added #
Zen.get<T>()— alias forZen.find<T>()Zen.remove<T>()— alias forZen.delete<T>()Zen.has<T>()— alias forZen.exists<T>()Zen.queryCache— shorthand forZenQueryCache.instance
1.6.2 #
Breaking Changes #
OptimisticMutationrenamed toZenMutation(helpers are now static methods)listAdd→listPut,listUpdate→listSet,add→put,update→set
// Before
OptimisticMutation.listAdd<Post>(queryKey: 'posts', mutationKey: 'create', mutationFn: ...)
// After
ZenMutation.listPut<Post>(queryKey: 'posts', mutationFn: ...) // mutationKey auto-generated
Changed #
mutationKeyis now optional; auto-generated fromqueryKeyif omitted
1.6.1 #
Added #
ZenMutation.listPut<T>,listSet<T>,listRemove<T>— optimistic list helpers with automatic rollbackZenMutation.put<T>,set<T>,remove()— optimistic single-value helpers
1.6.0 #
Added #
ZenStorageinterface for pluggable persistence backends (SharedPreferences, Hive, SQLite, etc.)- Auto-hydration: queries load cached data from storage on startup
- Offline mutation queue: mutations triggered while offline are queued and replayed on reconnect
ZenQueryCache.setQueryData()for function-based optimistic cache updatesNetworkModeenum:online,offlineFirst,alwaysexample/zen_offline— full offline-first example app
1.5.0 #
Breaking Changes #
refetchOnMount,refetchOnFocus,refetchOnReconnectchanged frombooltoRefetchBehaviorenum
// Before
ZenQueryConfig(refetchOnMount: true)
// After
ZenQueryConfig(refetchOnMount: RefetchBehavior.ifStale)
// New option: RefetchBehavior.always — force refetch regardless of staleness
Added #
RefetchBehavior.alwaysfor real-time/critical data use casesZenQueryConfig.retryDelayFn— custom function for error-aware retry delays
1.4.4 #
Fixed #
ZenInspectorOverlaycrashes when used without aScaffold— replacedScaffoldMessengerwith internal toast system- Layout overflows and data preview scrolling in inspector
1.4.3 #
1.4.1 #
Added #
ZenQueryClientandZenQueryClientOptionsfor managing global query defaultsZenQueryConfig.copyWith()for partial config overrides
1.4.0 #
Added #
pause()andresume()methods onZenQueryandZenStreamQueryZenQueryConfig.autoPauseOnBackgroundandrefetchOnResumeZenQueryCache.getAllQueries()for lifecycle management- Exponential backoff:
maxRetryDelay,retryBackoffMultiplier,retryWithJitter
Breaking Changes #
ZenStreamQuerynow defaults toautoPauseOnBackground: false(wastruein 1.3.x)
// To restore previous behavior
ZenStreamQuery(config: ZenQueryConfig(autoPauseOnBackground: true))
1.3.6 #
Fixed #
Zen.reset()no longer requirestestWidgets()— works in plaintest()blocksZenLifecycleManagerhandles missingWidgetsBindingin test environments gracefully
1.3.5 #
1.3.4 #
Changed #
- Fixed
createControllersyntax in all documentation - Rewrote
state_management_patterns.mdwith clearer examples
1.3.3 #
Fixed #
- 1-pixel
RenderFlexoverflow inZenInspectorOverlayon iOS/Android with no dependencies - Improved overlay positioning with explicit
Positioned.fill
1.3.2 #
Added #
ZenInspectorOverlay— in-app debug overlay with real-time scope hierarchy, query cache, and performance stats
1.3.1 #
Added #
- Automatic query tracking: queries created in
onInit()are automatically disposed when the controller closes — noonClose()boilerplate required
1.3.0 #
Changed #
- Internal architecture refactoring: replaced
ZenScopeManager(541 lines) andZenScopeStackTrackerwith a singleZen.currentScopenavigation bridge (~80% reduction in scope management code) ZenRoutereduced from 526 to 336 lines
Added #
parentScopeparameter onZenRoutefor explicit scope control
Breaking Changes #
- None. All public APIs unchanged.
1.2.3 #
Fixed #
ZenStreamQuerynow pauses oninactiveandhiddenapp states (web tab switching)- Lifecycle events (
onPause,onResume) now fire correctly when query is not registered in DI
1.2.1 #
Added #
ZenInfiniteQuery.getPreviousPageParam()andfetchPreviousPage()for bidirectional paginationhasPreviousPageandisFetchingPreviousPagereactive properties- Call-time callbacks on
mutate():onSuccess,onError,onSettled placeholderDatainZenQueryConfig— temporary initial data that doesn't persist to cachekeepPreviousDataonZenQueryBuilderandZenStreamQueryBuilderto prevent flash-of-loading on key changes
1.2.0 #
Added #
ZenStreamQuery— reactive wrapper forStream<T>with lifecycle management and optimistic updatesZenStreamQueryBuilderwidget
Changed #
- Folder restructure: builders moved to
lib/widgets/builders/, query internals tolib/query/core|logic|extensions/ import 'package:zenify/zenify.dart'is unaffected
1.1.9 #
Added #
ZenStorageinterface (foundation for 1.6.0 offline engine)persist,fromJson,toJson,storageoptions onZenQueryConfig- Auto-hydration from storage on query initialization
1.1.8 #
Breaking Changes #
- Fetcher signature now requires a
ZenCancelTokenparameter
// Before
fetcher: () => api.getData()
// After
fetcher: (_) => api.getData()
Added #
ZenCancelTokenfor platform-agnostic request cancellation- Automatic cancel on dispose and on re-trigger (prevents stale data race conditions)
1.1.7 #
1.1.6 #
Added #
enabledparameter andRxBool enabledproperty onZenQueryfor dependent queriesZenQueryCache.instance.prefetch()for pre-loading data without creating listeners
1.1.4 #
Added #
ZenInfiniteQueryfor paginated lists and infinite scrollingqueryKeynow acceptsObject(includingList) in addition toString
1.1.3 #
Added #
ZenMutationfor reactive write operations (create, update, delete)- Lifecycle hooks:
onMutate,onSuccess,onError,onSettled ZenQueryCache.invalidateQuery()integration pattern
1.1.1 #
Added #
scopeparameter onZenQueryfor automatic disposal with scope lifecycleZenQueryCache.invalidateScope(),refetchScope(),clearScope(),getScopeQueries(),getScopeStats()
1.1.0 #
Added #
ZenQuery— async state management with caching, deduplication, retries, and background refetchZenQueryBuilder— reactive widget with loading/error/success states and SWR patternZenQueryCache— global cache manager with invalidation and memory management
1.0.1 #
1.0.0 #
Breaking Changes #
EagerRef,LazyRef,ControllerRef,ZenRefreplaced by a singleRef<T>.put(),.asRef(),.register()extension methods removed — useZen.put()Zen.putFactory()removed — useZen.putLazy(..., isPermanent: false)- Debug methods moved to
ZenDebugclass:ZenDebug.dumpScopes()etc.
Added #
Ref<T>universal reference withcall()shorthand- Improved
ZenBuilderwith proper DI cleanup and ownership tracking
0.6.3 #
Added #
ZenEnvironment.productionVerboseenvironment presetZenConfig.shouldLogRoutesandshouldLogNavigationhelpers
0.6.2 #
Fixed #
- Inverted logic in
shouldLog()causing logs to appear when suppressed - Rx tracking logs now respect
ZenConfig.enableRxTracking
0.6.1 #
0.6.0 #
0.5.0 #
Initial pub.dev release.
0.4.1 #
0.4.0 #
0.3.0 #
Added #
RxResult<T>for success/failure error handling patternsRxComputedfor automatic dependency trackingRxFuturefor reactive async operations- Circuit breaker pattern for resilient reactive operations
- Batch operations and bulk updates for collections
0.1.7 #
0.1.5 #
Added #
- Hierarchical scope system
- Circular dependency detection
- Module/binding system for organized registration
- Lazy initialization support
0.1.4 #
Added #
- Generic type constraints on
RxList<E>,RxMap<K,V>,RxSet<E> ControllerRef<T>typed provider references
0.1.3 #
0.1.2 #
Fixed #
- Minimum Dart SDK updated to 2.19.0
- Deprecated
IndexErrorusage - Collection type safety in
RxList,RxMap,RxSet
0.1.1 #
Initial release — reactive state, controller lifecycle management, route-based disposal.