graft 0.1.0-alpha.1
graft: ^0.1.0-alpha.1 copied to clipboard
A high-performance, fine-grained reactive state management library for Flutter with automatic slot-level rebuild diffing, route-stack inheritance, and zero code-gen.
Graft ๐ฑ #
High-performance, fine-grained reactive state management for Flutter with zero boilerplate.
Important
๐ Graft is in Public Alpha (0.1.0-alpha.1) โ Let's Have a Ride! #
Graft is actively evolving in its initial alpha release. It is not yet declared production-ready. We warmly invite the Flutter developer community to take it for a spin: test it in your projects, push its slot diffing and route scoping to the limits, and help us make it even better!
Share your feedback, edge cases, and ideas on GitHub Issues or join the discussions as we march towards 1.0!
๐ฆ Installation #
Add graft to your pubspec.yaml:
flutter pub add graft
Or manually add it to dependencies:
dependencies:
graft: ^0.1.0-alpha.1
Import it in your Dart code:
import 'package:graft/graft.dart';
๐ Why Graft? #
Most Flutter state management solutions force you into an unpleasant compromise:
- Flutter Bloc gives you clean architecture and observability, but punishes you with a "Pyramid of Doom" (nesting 5
BlocSelectorwidgets just to avoid rebuilding 5 fields in a Column), plus heavy boilerplate. - Riverpod offers dependency injection, but pushes heavily toward code generation (
@riverpod,build_runner,.g.dartclutter), replaces standardStatelessWidgetwithConsumerWidget, and requires threadingWidgetRefeverywhere. - Signals / Solidart offer fine-grained rebuilds, but fragment your state into dozens of loose primitive variables, destroying cohesive domain models.
- GetX bypasses Flutter's Element tree and route lifecycles with global mutable state and untyped string lookups, leading to memory leaks.
Graft solves all of this.
| Feature / Metric | Flutter BLoC / Cubit | Riverpod | Provider | GetX | MobX | Signals | Graft |
|---|---|---|---|---|---|---|---|
| Fine-Grained Rebuilds | โ Manual BlocSelector per field |
โ ๏ธ Requires ref.watch(p.select(...)) |
โ Manual Selector per field |
โ ๏ธ Obx(() => ...) wrappers everywhere |
โ ๏ธ Observer wrappers everywhere |
โ Micro-rebuild per signal | โ
Automatic: graft.slots(layout: ..., children: ...) diffs slots with zero manual selectors |
| Widget Tree Nesting | โ Deep pyramid (BlocProvider โ BlocBuilder) |
โ ๏ธ ConsumerWidget or Consumer |
โ Deep pyramid (ChangeNotifierProvider โ Consumer) |
โ Minimal | โ ๏ธ Observer wrappers |
โ ๏ธ Watch(...) wrappers |
โ
Zero Nesting: final graft = context.use<MyGraft>() at top of standard StatelessWidget |
| Code Generation | โ None | โ Heavily pushed (@riverpod, build_runner) |
โ None | โ None | โ Mandatory (@observable, @action) |
โ None | โ Strictly 0 Code-Gen |
| State Structure | โ Single cohesive domain class | โ Single cohesive domain class | โ Single cohesive domain class | โ Fragmented reactive vars (.obs) |
โ Fragmented observables | โ Fragmented loose signals (signal()) |
โ Single cohesive domain State |
| Route Stack Sharing | โ ๏ธ Manual BlocProvider.value |
โ ๏ธ autoDispose or manual overrides |
โ ๏ธ Manual scoping | โ Global map (frequent memory leaks) | โ ๏ธ Manual dispose() |
โ ๏ธ Manual disposal of effects | โ Automatic: Inherits from predecessor routes; auto-disposes when owner pops |
| Subclass Boilerplate | โ ๏ธ High (Events, States, Handlers) | โ ๏ธ High (family providers, code-gen) | โ ๏ธ Moderate (ChangeNotifier) |
โ ๏ธ Moderate | โ ๏ธ High (.g.dart store files) |
โ ๏ธ High (declaring 10+ signals) | โ
Zero: class InfoGraft extends Graft<InfoState> |
| Observability | โ
BlocObserver |
โ ๏ธ ProviderObserver |
โ None built-in | โ ๏ธ Basic prints | โ ๏ธ MobX spy | โ None built-in | โ
GraftObserver & colorized GraftDevObserver |
| Unit Testing | โ
Declarative blocTest |
โ ๏ธ ProviderContainer manual tests |
โ ๏ธ Manual mocks | โ ๏ธ Difficult to isolate | โ ๏ธ Manual harness | โ ๏ธ Manual effects | โ
Declarative graftTest (Pure Dart, sub-millisecond) |
โก Developer Ergonomics: Column Rebuild Comparison #
How do state management libraries compare when trying to achieve fine-grained, single-widget rebuilds in a multi-child Column?
1. Flutter BLoC / Cubit (Pyramid of Selectors): #
// โ BLoC: Requires wrapping EVERY single dynamic widget in a verbose BlocSelector
Column(
children: [
const HeaderBanner(),
BlocSelector<UserBloc, UserState, String>(
selector: (s) => s.name,
builder: (context, name) => CkText(text: name),
),
BlocSelector<UserBloc, UserState, String>(
selector: (s) => s.email,
builder: (context, email) => Text(email),
),
BlocSelector<UserBloc, UserState, bool>(
selector: (s) => s.isVerified,
builder: (context, verified) => verified ? const VerifiedBadge() : const SizedBox(),
),
],
)
2. Riverpod (Fragmented Consumers): #
// โ ๏ธ Riverpod: Requires splitting into multiple Consumer widgets or writing ref.watch selectors
Column(
children: [
const HeaderBanner(),
Consumer(builder: (context, ref, _) {
final name = ref.watch(userProvider.select((s) => s.name));
return CkText(text: name);
}),
Consumer(builder: (context, ref, _) {
final email = ref.watch(userProvider.select((s) => s.email));
return Text(email);
}),
Consumer(builder: (context, ref, _) {
final isVerified = ref.watch(userProvider.select((s) => s.isVerified));
return isVerified ? const VerifiedBadge() : const SizedBox();
}),
],
)
3. With Graft (Clean, Harmonized Named Syntax): #
// โ
Graft: Zero selectors, zero boilerplate.
// The slot engine automatically isolates each child widget!
graft.slots(
layout: (children) => Column(children: children), // Explicit layout is required!
children: (s) => [
const HeaderBanner(), // 0 rebuilds (pointer match)
CkText(text: s.name), // 0 rebuilds when name is unchanged (auto-unwrapped & diffed)
Text(s.email), // 0 rebuilds when email is unchanged
if (s.isVerified) const VerifiedBadge(),
],
)
๐๏ธ Performance & Render Pipeline Benchmark #
The 10,000x Cost Difference: #
In standard Flutter and BLoC (BlocBuilder), every state emission forces the entire child subtree through Flutter's expensive rendering pipeline:
Widget.build()allocationElement.update()&Element.rebuild()RenderObject.markNeedsLayout()RenderObject.performLayout()RenderObject.markNeedsPaint()RenderObject.paint()
Cost of full Render Pipeline: ~1.0 to 5.0 milliseconds per frame.
In Graft (graft.slots):
- Diffing occurs strictly in memory before touching Flutter elements:
constpointer check (identical(a, b)): < 1 nanosecond- Keyed check (
a.key == b.key): ~5 nanoseconds - Primitive property inspection (strings, padding, alignment): ~15 to 80 nanoseconds
- If properties match, Flutter's Element is never dirtied. Layout is skipped, and paint is skipped completely!
- Pure Dart diffing is ~10,000x faster than dirtying the Flutter RenderObject tree.
Verified Automated Benchmark (10-Slot Column, 60 State Updates): #
From our automated benchmark test (test/benchmark/column_rebuild_benchmark_test.dart):
================================================================================
GRAFT REBUILD BENCHMARK (10-Slot Column, 60 State Updates)
================================================================================
Traditional / Monolithic Rebuild Model:
- Children in Column: 10
- Frames / Updates: 60
- Total Child Builds: 600 rebuilds (10 children * 60 updates)
Graft Fine-Grained slot engine:
- Slot 0 (Changing): 60 rebuilds (1 per frame)
- Slots 1-9 (Static): 0 rebuilds across all 60 frames
- Total Child Builds: 60 rebuilds
- Total Elapsed Time: ~51 ms
- Rebuild Reduction: 90.0% LESS WORK!
================================================================================
๐ ๏ธ Quick Start #
1. Define State & Controller: #
class UserState extends GraftState {
String name = 'Alice';
String email = 'alice@example.com';
bool isVerified = false;
}
class UserGraft extends Graft<UserState> {
UserGraft() : super(UserState());
void updateName(String newName) {
state
..name = newName
..update(); // Batched diffing: notifies listeners and diffs UI slots
}
void updateEmail(String newEmail) {
state
..email = newEmail
..update();
}
void toggleVerified() {
state
..isVerified = !state.isVerified
..update();
}
}
2. Setup Route Tracking in main.dart: #
void main() {
// Optional: Enable dev telemetry
Graft.observer = GraftDevObserver(logRebuilds: true);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// Register route tracker to enable automatic route-scoped lifecycle disposal!
navigatorObservers: [GraftRouteTracker.observer],
home: const UserScreen(),
);
}
}
3. Consume in UI with Zero Boilerplate: #
class UserScreen extends StatelessWidget {
const UserScreen({super.key});
@override
Widget build(BuildContext context) {
final graft = context.use<UserGraft>();
return Scaffold(
appBar: AppBar(
title: graft.slot(
builder: (s) => Text(s.name),
),
),
body: graft.slots(
layout: (children) => Column(children: children), // Required layout wrapper
children: (s) => [
const HeaderBanner(),
Text('Email: ${s.email}'),
if (s.isVerified) const VerifiedBadge(),
ElevatedButton(
onPressed: () => graft.updateName('Bob'),
child: const Text('Rename to Bob'),
),
],
),
);
}
}
๐จ Harmonized Widget Slot API #
Graft provides a clean, 100% named-parameter API:
1. graft.slot({required builder, key}) (Single-Child Slot) #
Isolates any single widget slot. Only rebuilds when the returned widget changes:
AppBar(
title: graft.slot(
builder: (s) => Text(s.title),
),
)
// Also handles full-page state switching:
graft.slot(
builder: (s) {
if (s.isLoading) return const CircularProgressIndicator();
if (s.hasError) return Text(s.error);
return ContentView(data: s.data);
},
)
2. graft.slots({required layout, required children, key}) (Multi-Child Diffing Layout) #
Automatically diffs every child widget independently. layout: is required (no implicit layouts):
// Column layout:
graft.slots(
layout: (children) => Column(children: children),
children: (s) => [
const HeaderBanner(),
Text(s.name),
if (s.isVerified) const VerifiedBadge(),
Text(s.email),
],
)
// Custom Layout (Row, Wrap, Stack):
graft.slots(
layout: (children) => Row(children: children),
children: (s) => [
const Icon(Icons.star),
Text('${s.rating}'),
Text('(${s.reviews})'),
],
)
3. graft.compute<R>({required compute, required builder, key}) (Pre-Flight Derived Computation) #
Computes a derived value from state first. If the computed value is unchanged, the widget builder is never even executed, saving CPU cycles on heavy subtrees:
graft.compute<int>(
compute: (s) => s.notifications.length,
builder: (count) => HeavyBadge(count: count),
)
4. graft.builder<T>({required items, required itemBuilder, itemKey, layout, key}) (Virtualized Collections) #
Zero-rebuild diffing for virtualized collections (ListView, GridView, SliverList).
items: passed directly once.itemBuilder: receives(item, index)(no redundantBuildContext).layout: defaults automatically toListView.builder.
graft.builder<TaskItem>(
items: s.tasks,
itemBuilder: (task, index) => ListTile(
title: Text(task.title),
trailing: Checkbox(
value: task.isDone,
onChanged: (_) => graft.toggleTask(task.id),
),
),
)
// Override layout for GridView or CustomScrollView:
graft.builder<Product>(
items: s.products,
layout: (itemCount, itemBuilder) => GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: itemCount,
itemBuilder: itemBuilder,
),
itemBuilder: (product, index) => ProductCard(product: product),
)
5. valueGraft.slot({required builder, key}) (Micro-State Cells) #
For lightweight single primitives (int, bool, String):
final count = ValueGraft<int>(0);
count.slot(
builder: (val) => Text('Count: $val'),
)
โจ๏ธ Developer Ergonomics: IDE Snippets #
Graft includes pre-configured snippets for VS Code / Cursor and live templates for Android Studio / IntelliJ:
Run the installer from your project root:
dart run graft:snippets
Available Snippets: #
graft-controller: ScaffoldGraft+GraftStatepair.graft-value: Scaffold lightweightValueGraft.graft-slot: Single-child isolated slotgraft.slot(builder: (s) => ...).graft-slots: Multi-child isolated slot layout with requiredlayout:parameter.graft-builder: Virtualized collection buildergraft.builder<T>(items: ..., itemBuilder: ...).graft-compute: Pre-flight computed slotgraft.compute<T>(compute: ..., builder: ...).graft-test: Pure Dart declarative unit test template.
๐ Compile-Time Lint Enforcement: graft_lint #
Catch anti-patterns directly in your IDE with the official Graft analyzer plugin:
Add to dev_dependencies:
dev_dependencies:
custom_lint: ^0.8.1
graft_lint: ^0.1.0-alpha.1
Enable in analysis_options.yaml:
analyzer:
plugins:
- custom_lint
Included Rules: #
avoid_nested_graft_slot: Warns at compile-time if agraft.slot()is redundantly nested inside another slot or slots engine.require_graft_route_observer: Reminds you to registerGraftRouteTracker.observerinMaterialApp.navigatorObservers.
๐งช Declarative Unit Testing: graftTest #
Test business logic in pure Dart without widget tree overhead:
import 'package:flutter_test/flutter_test.dart';
import 'package:graft/graft.dart';
import 'package:graft/testing.dart';
void main() {
group('UserGraft', () {
graftTest<UserGraft, UserState>(
'updates name when updateName is called',
build: () => UserGraft(),
act: (graft) => graft.updateName('Bob'),
expect: () => [
isA<UserState>().having((s) => s.name, 'name', 'Bob'),
],
verify: (graft) {
expect(graft.state.name, 'Bob');
},
);
});
}
๐ License #
MIT License. Free to use, modify, and distribute.