flutter_zenith 0.1.0
flutter_zenith: ^0.1.0 copied to clipboard
A lightweight container-scoped Flutter state management and DI library.
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 #
- 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 withref.isMountedexecution tokens eliminate asynchronous post-disposal crashes. - Zero Dependencies: Pure Dart/Flutter framework implementationβno
build_runner, code generation, or third-party packages required.
π¦ 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 {
userProfileNode.set(AsyncLoading(userProfileNode.value.valueOrNull));
try {
final name = await Future.delayed(const Duration(seconds: 1), () => 'John Doe');
// Always guard async state mutations with ref.isMounted
if (!ref.isMounted) return;
userProfileNode.set(AsyncData(name));
} catch (e, st) {
if (!ref.isMounted) return;
userProfileNode.set(AsyncError(e, st));
}
}
}
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});
@override
Widget build(BuildContext context) {
final container = ZenithScope.of(context);
final controller = container.getOrCreateNode(
'counterController',
(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),
),
);
}
}
π€ 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 #
- Async Execution Guards: ALL asynchronous operations (
Future,Stream, orTimercallbacks) MUST checkref.isMountedbefore setting or mutating anyZenithNodevalue. - Node Mutation Checks: Do NOT attempt to read or mutate a disposed
ZenithNode. Always ensure node disposal is handled throughZenithContainer.dispose()orref.onDispose().
// β
CORRECT AI PATTERN:
Future<void> loadData(ZenithRef ref) async {
final result = await api.fetch();
if (!ref.isMounted) return; // REQUIRED GUARD
node.set(AsyncData(result));
}
// β INCORRECT AI PATTERN (DO NOT GENERATE):
Future<void> loadData(ZenithRef ref) async {
final result = await api.fetch();
node.set(AsyncData(result)); // DANGEROUS: May execute post-disposal
}
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_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.