flutter_zenith 0.7.0
flutter_zenith: ^0.7.0 copied to clipboard
A high-performance, container-scoped state management and dependency injection engine for Flutter. Zero-dependency, surgical rendering, async lifecycle guards, and type-safe DI — all in one package.
flutter_zenith #
A high-performance, container-scoped state management and dependency injection engine for Flutter.
flutter_zenith merges Container-Based Scoped Memory Isolation (like Riverpod) with Surgical Graph Reactivity (like Signals) and First-Class Async Lifecycle Guards (like BLoC) into a single, zero-dependency engine.
🌟 Key Features #
- Type-Safe DI Keys:
ZenithKey<T>gives compile-time-safe dependency lookup, replacing string-only key workflows when desired. - Scoped Overrides:
ZenithOverride<T>lets tests and local scopes swap implementations without changing production factories. - Surgical Rendering: Widgets subscribe directly to atomic property nodes (
ZenithNode<T>). Mutating a node re-renders only the listeningZenithBuilder—parent and sibling widgets remain completely idle. - Scoped Memory Isolation: State resides inside a
ZenithContainerbound to the widget tree viaZenithScope. Swapping or unmounting a scope automatically purges all contained state from memory. - Weak Reference Memory Safety: Node subscriptions use
WeakReferenceand automatic subscriber pruning to prevent memory leaks, even if a listener fails to unsubscribe. - Async Lifecycle Guards: Built-in
AsyncValue<T>state machines (AsyncData,AsyncLoading,AsyncError) paired with automatic mounted-checks viaref.set()/ref.runAsync()eliminate asynchronous post-disposal crashes without manualref.isMountedchecks. - Concurrency & Isolate Guards:
ref.runAsyncGuarded(node, task, strategy: ...)solves race conditions between overlapping async calls (droppablefor double-submit prevention,restartablefor stale-response prevention), andref.runInIsolate(node, payload, heavyComputation)offloads heavy synchronous work to a backgroundIsolatewithout leaving theAsyncValuestate machine. - Zero Dependencies: Pure Dart/Flutter framework implementation—no
build_runner, code generation, or third-party packages required.
What Is New in 0.7.0 #
- Feature Management & Feature Flags: Introduced
ZenithFeature,FeatureNode, andZenithFeatureBuilderwidget for reactive feature flag rollouts and percentage rule evaluation. - Options Pattern: Introduced
ZenithConfigNode<T>(ASP.NET CoreIOptionsMonitor<T>equivalent) for dynamic JSON/Map configuration decoding. - Environment Profiles: Added
ZenithEnvironment(development,staging,production) and environment-specific DI overrides inZenithContainer. - Hosted Background Services: Added
ZenithServicelifecycle worker contract with concrete implementationsZenithPeriodicServiceandZenithIsolateService.
What Was New in 0.6.0 #
- In-Place Container Reset: Added
container.reset()to teardown all nodes and references in an existing container without destroying the container instance. - Auth State Machine: Added
AuthState<T>(AuthUnauthenticated,AuthAuthenticating,AuthAuthenticated<T>,AuthError) with exhaustive pattern matching and boolean status getters. - State Persistence: Introduced
ZenithStorageinterface,InMemoryStorage, andPersistedNode<T>for auto-syncing state across app restarts. - Node Middleware: Added
ZenithMiddleware<T>interceptor pipeline (onWillSet,onDidSet) for value sanitization, write blocking, and audit logging.
What Was New in 0.5.0 #
AsyncValueValue Equality: Implemented==andhashCodeforAsyncData,AsyncLoading, andAsyncError. Re-setting identical async values now skips unnecessary subscriber notifications and widget rebuilds.- Stream Integration: Added
ref.watchStream(node, stream)andnode.watch(ref, stream)for auto-subscribingStreams toAsyncValuenodes with automatic lifecycle teardown on scope disposal. - Computed Nodes: Introduced
ComputedNode<T>for derived state calculations with automatic source node dependency tracking and memoized invalidation. - Granular Subscriptions: Added
ZenithSelector<T, R>widget for selecting derived properties from a node, preventing rebuilds when unrelated fields change. - Framework Observer: Added
ZenithObserverandZenithLogObserverfor debugging node creation, mutations, and container disposal events. - Builder Error Handling: Added
errorBuilderparameter toZenithBuilderfor graceful error rendering when builder closures throw.
What Was New in 0.4.0 #
- Added
ConcurrencyStrategy(concurrent,droppable,restartable) andZenithConcurrencyX.runAsyncGuarded(node, task, {strategy}): guardsAsyncValue<T>nodes against race conditions from overlapping async calls --droppableignores new calls while one is already loading;restartable(the default) discards stale results from superseded calls. - Added
ZenithIsolateX.runInIsolate(node, payload, heavyComputation, {strategy}): runs a heavy computation on a backgroundIsolateviaIsolate.run, then writes the result throughrunAsyncGuarded.
What Was New in 0.3.0 #
- Added
ZenithRef.set(node, value)andZenithRef.runAsync(node, task): mounted-safe writes that silently no-op once the ref's scope is disposed, so you no longer need to manually checkref.isMountedafter everyawait. - Added the
SafeAsyncNodeX.guard(ref, future)extension onZenithNode<AsyncValue<T>>as sugar overref.runAsync(...). - Breaking:
ZenithNode.set()no longer throwsStateErrorwhen called on a disposed node — it now fails silently (a no-op). Reading.value, calling.invalidate(), or.subscribe()on a disposed node still throwStateErroras before.
What Was New in 0.2.0 #
- Added
ZenithKey<T>and typed container helpers:getOrCreate,maybeReadKey,invalidateKey. - Added scoped overrides with
ZenithOverride<T>. - Added
BuildContexthelpers:context.containerandcontext.zenith(...). - Existing string-key API remains supported for backwards compatibility.
📦 Installation & Setup #
Add flutter_zenith to your pubspec.yaml:
dependencies:
flutter_zenith:
path: ../flutter_zenith # or package version
Import the package in your Dart code:
import 'package:flutter_zenith/flutter_zenith.dart';
🚀 Quick Start Guide #
1. Create State Nodes & Controllers #
State in Zenith is encapsulated in ZenithNode<T> instances. For async workflows, use AsyncValue<T>:
class CounterController {
// Synchronous atomic node
final counterNode = ZenithNode<int>(0);
// Async state node
final userProfileNode = ZenithNode<AsyncValue<String>>(const AsyncLoading());
void increment() {
counterNode.set(counterNode.value + 1);
}
Future<void> fetchUser(ZenithRef ref) async {
// ref.runAsync() automatically manages the loading/data/error
// transitions and drops the result if the scope is disposed mid-flight
// -- no manual `if (!ref.isMounted) return;` checks required.
await ref.runAsync(userProfileNode, () {
return Future.delayed(const Duration(seconds: 1), () => 'John Doe');
});
}
}
Prefer a more explicit call site? Use ref.set() for one-off mounted-safe writes:
Future<void> fetchUserManually(ZenithRef ref) async {
final name = await api.fetchUser();
ref.set(userProfileNode, AsyncData(name)); // Silently ignored if disposed.
}
Race Conditions & Double-Submits: ref.runAsyncGuarded()
When an async call can be triggered again before it finishes (rapid search
typing, a "Pay" button tapped twice), use runAsyncGuarded instead of
runAsync:
// Search-as-you-type: only the LATEST query is ever allowed to update state.
void onSearchQueryChanged(ZenithRef ref, String query) {
ref.runAsyncGuarded(
searchResultsNode,
() => api.search(query),
strategy: ConcurrencyStrategy.restartable, // default
);
}
// Submit button: ignore rapid re-taps while a request is already in flight.
Future<void> submitPayment(ZenithRef ref) async {
await ref.runAsyncGuarded(
paymentStateNode,
() => api.processPayment(),
strategy: ConcurrencyStrategy.droppable,
);
}
Heavy Computation Off the UI Thread: ref.runInIsolate()
For CPU-heavy synchronous work (large JSON parsing, crypto), offload it to a
background Isolate while keeping the same AsyncValue state machine:
Future<void> loadLargeDataset(ZenithRef ref, String rawJsonResponse) async {
await ref.runInIsolate(
datasetNode,
rawJsonResponse,
(json) {
// Runs on a background Isolate -- keep this pure and self-contained;
// do not close over ZenithRef, BuildContext, or other mutable state.
final list = jsonDecode(json) as List;
return list.map((item) => HeavyModel.fromJson(item)).toList();
},
);
}
2. Wrap the Scope in the Widget Tree #
Bind state lifecycles to your widget hierarchy using ZenithScope and ZenithContainer:
class MyApp extends StatefulWidget {
final String userId;
const MyApp({super.key, required this.userId});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final ZenithContainer _container;
@override
void initState() {
super.initState();
_container = ZenithContainer();
}
@override
Widget build(BuildContext context) {
// Changing the ValueKey automatically disposes the old container and creates a clean scope
return ZenithScope(
key: ValueKey(widget.userId),
container: _container,
child: const MaterialApp(
home: HomeScreen(),
),
);
}
}
3. Build Surgically Reactive UI #
Use ZenithBuilder to read nodes. The builder automatically registers as a listener to any ZenithNode read inside its builder execution zone:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
static const counterControllerKey = ZenithKey<CounterController>(
'counterController',
);
@override
Widget build(BuildContext context) {
final controller = context.zenith(
counterControllerKey,
(ref) => CounterController(),
).value;
return Scaffold(
appBar: AppBar(title: const Text('Zenith Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ONLY this ZenithBuilder rebuilds when counterNode changes
ZenithBuilder(
builder: (context) {
final count = controller.counterNode.value;
return Text('Count: $count', style: const TextStyle(fontSize: 24));
},
),
const SizedBox(height: 20),
// Pattern-matching Async State
ZenithBuilder(
builder: (context) {
return controller.userProfileNode.value.when(
loading: (prev) => Text('Loading... (Previous: $prev)'),
error: (err, stack) => Text('Error: $err'),
data: (name) => Text('Welcome, $name!'),
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
Optional Scoped Overrides (Testing / Feature Scopes) #
const counterKey = ZenithKey<CounterController>('counterController');
final container = ZenithContainer(
overrides: [
ZenithOverride<CounterController>(
counterKey,
(ref) => CounterController(),
),
],
);
🤖 AI Agent Developer Instructions & Rules #
If you are an AI coding assistant (Cursor, Claude Code, Copilot, Devin, etc.) generating or refactoring code for a project using flutter_zenith, you must strictly adhere to the following rules:
Rule 1: Zero External State Management Dependencies #
- Do NOT import or introduce
flutter_riverpod,signals,flutter_bloc,provider, orget_it. - Rely exclusively on
package:flutter_zenith/flutter_zenith.dart.
Rule 2: Memory & Async Safety #
- Prefer
ref.runAsync()/ref.set(): ALL asynchronous operations (Future,Stream, orTimercallbacks) that write to aZenithNodeSHOULD useref.runAsync(node, task)(forAsyncValue<T>nodes) orref.set(node, value)(for plain nodes) instead of manually checkingref.isMounted. Both silently no-op once the ref's scope is disposed. - Use
ref.runAsyncGuarded()for rapid-fire triggers: If aZenithNode<AsyncValue<T>>can be re-triggered before the previous call finishes (search-as-you-type, fast tab switching, a submit button tapped repeatedly), useref.runAsyncGuarded(node, task, strategy: ...)instead of bareref.runAsync(). UseConcurrencyStrategy.restartable(default) so only the latest call's result lands, orConcurrencyStrategy.droppableto ignore new calls while one is already loading. - Use
ref.runInIsolate()for heavy synchronous work: Large JSON parsing, crypto, or other CPU-bound transforms that would jank the UI thread SHOULD run viaref.runInIsolate(node, payload, heavyComputation), not directly insiderunAsync/runAsyncGuarded. KeepheavyComputationfree of closures overZenithRef,BuildContext, or other non-sendable state. - Manual guard as fallback: If a helper isn't a good fit, checking
if (!ref.isMounted) return;before mutation is still valid and supported. - Node Mutation Checks: Do NOT attempt to read a disposed
ZenithNode(.valuethrowsStateError). Always ensure node disposal is handled throughZenithContainer.dispose()orref.onDispose(). Note thatZenithNode.set()itself is a safe no-op on a disposed node (it does not throw), butref.runAsync()/ref.set()remain the preferred, self-guarding entry points.
// ✅ CORRECT AI PATTERN:
Future<void> loadData(ZenithRef ref) async {
await ref.runAsync(node, () => api.fetch());
}
// ✅ ALSO CORRECT (manual write, still mounted-safe):
Future<void> loadDataManually(ZenithRef ref) async {
final result = await api.fetch();
ref.set(node, AsyncData(result));
}
// ❌ AVOID (unnecessary manual guard -- use ref.runAsync()/ref.set() instead):
Future<void> loadData(ZenithRef ref) async {
final result = await api.fetch();
if (!ref.isMounted) return;
node.set(AsyncData(result));
}
Rule 3: Surgical Building Constraints #
- Wrap ONLY the minimal required widget subtree in a
ZenithBuilder. - Do NOT place
ZenithBuilderat the root of large screen scaffolds unless every element on the screen depends on the read nodes. - To prevent unnecessary observer zone leakage, perform all
ZenithNode.valuereads directly inside theZenithBuilder.builderclosure.
// ✅ CORRECT (Surgical):
Widget build(BuildContext context) {
return Column(
children: [
const StaticHeaderWidget(), // Will NOT rebuild
ZenithBuilder(
builder: (context) => Text(node.value), // ONLY this rebuilds
),
],
);
}
// ❌ INCORRECT (Broad Rebuild):
Widget build(BuildContext context) {
return ZenithBuilder(
builder: (context) {
return Column(
children: [
const StaticHeaderWidget(), // Rebuilds unnecessarily
Text(node.value),
],
);
},
);
}
Rule 4: Container & Scope Management #
- Access the active container via
ZenithScope.of(context). - Use
ZenithContainer.getOrCreateNode(key, factory)to instantiate controllers or services. - Always assign a distinct
ValueKeytoZenithScopewhen user sessions change (e.g., login/logout) to force container teardown and prevent state cross-contamination.
📂 Package Architecture & Directory Structure #
flutter_zenith/
├── lib/
│ ├── flutter_zenith.dart # Main export barrel file
│ └── core/
│ └── zenith/
│ ├── async_value.dart # AsyncValue state machine (Data, Loading, Error)
│ ├── zenith_concurrency.dart # ConcurrencyStrategy, runAsyncGuarded, runInIsolate
│ ├── zenith_node.dart # Atomic reactive nodes with WeakRef tracking
│ ├── zenith_container.dart # Memory DI container, ref lifecycle, & scoping
│ ├── zenith_widgets.dart # ZenithScope & surgical ZenithBuilder
│ └── zenith_core.dart # Internal framework barrel
├── test/
│ └── core/
│ └── zenith/
│ ├── zenith_lifecycle_test.dart # Teardown & isMounted validation
│ ├── zenith_memory_test.dart # Weak reference & GC pruning tests
│ ├── zenith_widget_test.dart # Rebuild isolation verification
│ ├── zenith_scope_test.dart # Scope swap & disposal tests
│ └── zenith_benchmark_widget_test.dart # High-load stress & performance tests
└── example/ # Runnable demo app
🧪 Running Tests & Benchmarks #
Run the complete framework test suite:
flutter test
Run static code analysis:
flutter analyze
Run the runnable example application:
cd example
flutter pub get
flutter run
📜 License #
Distributed under the MIT License. See LICENSE for more information.