fairy 2.0.0
fairy: ^2.0.0 copied to clipboard
A lightweight MVVM framework for Flutter with strongly-typed, reactive data binding state management library, Mainly focused on simplicity and ease of use.
2.0.0 #
V2 Major Release - Cleaner API with better error handling and simplified dependency injection.
๐ Breaking Changes #
1. Bind Widget: selector: โ bind: Parameter
Migration: Find and replace selector: with bind: in all Bind widgets.
// Before (V1)
Bind<UserViewModel, String>(
selector: (vm) => vm.userName,
builder: (context, value, update) => TextField(...),
)
// After (V2)
Bind<UserViewModel, String>(
bind: (vm) => vm.userName,
builder: (context, value, update) => TextField(...),
)
2. FairyLocator: Remove .instance
Migration: Find and replace FairyLocator.instance. with FairyLocator.
// Before (V1)
FairyLocator.instance.registerSingleton<ApiService>(ApiService());
final api = FairyLocator.instance.get<ApiService>();
// After (V2)
FairyLocator.registerSingleton<ApiService>(ApiService());
final api = FairyLocator.get<ApiService>();
All methods are now static: registerSingleton, registerLazySingleton, registerFactory, get, isRegistered, unregister, reset.
โจ New Features #
Command Error Handling with onError Callback
Commands now support error handling at the UI level via optional onError callback. Available on all command types:
class LoginViewModel extends ObservableObject {
final errorMessage = ObservableProperty<String?>(null);
late final loginCommand = AsyncRelayCommand(
_login,
onError: (error, stackTrace) {
errorMessage.value = 'Login failed: $error';
logger.error('Login error', error, stackTrace);
},
);
Future<void> _login() async {
errorMessage.value = null; // Clear previous errors
await authService.login(email.value, password.value);
}
}
// Display errors using Bind (consistent with "Learn 2 widgets" philosophy)
Bind<LoginViewModel, String?>(
bind: (vm) => vm.errorMessage,
builder: (context, error, _) {
if (error == null) return SizedBox.shrink();
return ErrorCard(error);
},
)
Key Points:
- Errors stored as state in ViewModel (
ObservableProperty) - Display errors via
Bindwidget (consistent pattern) - Optional callback - only add when needed
- Available on:
RelayCommand,AsyncRelayCommand,RelayCommandWithParam<T>,AsyncRelayCommandWithParam<T>
๐ฏ Design Decisions #
Why No context.watch<T>() or ref.watch<T>() Style Extensions?
Deliberate omission to maintain Fairy's core philosophy and simplicity:
- Widget-based API keeps it simple:
BindandCommandwidgets handle all reactive needs without requiring customStatelessWidget/StatefulWidgetbase classes - No framework coupling: Unlike Provider/Riverpod which require
ConsumerWidget/ConsumerStatefulWidget, Fairy works with standard Flutter widgets - Avoid complexity: Listenable extensions would require new widget types and lifecycle patterns, contradicting the "Learn 2 widgets" philosophy
- Explicit over implicit:
Bindwidgets make reactive dependencies visible and auditable in the widget tree
For imperative access (e.g., calling commands in callbacks), Fairy.of<T>(context) provides read-only ViewModel access. For overlays, use FairyBridge widget.
๐ Documentation #
- Added comprehensive command error handling examples (basic, type-safe, snackbar patterns)
- Updated Quick Reference table with error handling capabilities
- Updated all examples to use V2 API (
bind:parameter, staticFairyLocator) - Enhanced Common Patterns section with error handling integration
๐งช Testing #
- 574 tests passing (up from 565 in v1.4.0)
- Added 9 tests for command error handling scenarios
- All breaking changes validated with updated tests
๐ Performance #
- Benchmarks updated with V2 measurements
- Performance characteristics maintained from v1.4.0
- Memory: 112.6% baseline, Widget: 112.7% baseline
- Selective Rebuilds: 100% baseline (31-34% faster than Provider/Riverpod)
- Auto-tracking: 100% baseline (26-33% faster than Provider/Riverpod)
๐ก Migration Effort #
Small projects (< 10 files): 10-15 minutes
Medium projects (10-50 files): 20-30 minutes
Large projects (50+ files): 30-60 minutes
Migration steps:
- Replace
selector:โbind:(find & replace) - Replace
FairyLocator.instance.โFairyLocator.(find & replace) - Test thoroughly
- Optionally add error handling where needed
๐ Release & Support #
Introduced formal versioning policy: Fairy now follows a non-breaking minor version principle with clear support cadence. See README for full details on version compatibility and support policy.
1.4.0 #
Auto-Tracking for Commands - Commands now automatically track when accessed in Bind.viewModel, enabling reactive UI updates without explicit Command widget usage.
โจ New Features #
- Command.canExecute auto-tracking - All command
canExecutegetters/methods now report access toDependencyTrackerRelayCommand.canExecutegetter automatically trackedAsyncRelayCommand.canExecutegetter automatically trackedRelayCommandWithParam<T>.canExecute(param)method automatically trackedAsyncRelayCommandWithParam<T>.canExecute(param)method automatically tracked- Impact:
Bind.viewModelnow rebuilds whennotifyCanExecuteChanged()is called, even without usingCommandwidget
๐ฏ Benefits #
Before (v1.3.x):
Bind.viewModel<MyViewModel>(
builder: (context, vm) {
// โ This won't rebuild when canExecute changes!
return ElevatedButton(
onPressed: vm.saveCommand.canExecute ? vm.saveCommand.execute : null,
child: Text('Save'),
);
},
)
After (v1.4.0):
Bind.viewModel<MyViewModel>(
builder: (context, vm) {
// โ
Automatically rebuilds when canExecute changes!
return ElevatedButton(
onPressed: vm.saveCommand.canExecute ? vm.saveCommand.execute : null,
child: Text('Save'),
);
},
)
๐ Migration Guide #
No code changes required! This is fully backward compatible:
- โ
Existing
Commandwidget usage โ Works exactly the same - โ
Direct command access in
Bind.viewModelโ Now auto-tracks (was broken before) - โ
Direct command
execute()calls โ Works exactly the same
๐งช Testing #
- Added 7 comprehensive tests for command auto-tracking in
Bind.viewModel - Added 6 tests for command selector patterns and edge cases
- All 565 tests passing
๐ Performance #
- Improved benchmark methodology with noise reduction (warm-up + median of 5 measurements)
- Rebuild Performance: 24-32% faster than Provider/Riverpod with auto-tracking
- Selective Rebuilds: 30-38% faster with explicit binding
- Memory Trade-off: Intentional 14% increase for superior rebuild performance and DX
Notes #
- Fully backward compatible - no breaking changes
- Improves developer experience by making command usage more intuitive
- Commands now behave consistently with
ObservablePropertyauto-tracking
1.3.5+1 #
Documentation Refinement - Improved clarity and completeness of binding patterns and disposal utilities.
๐ Documentation #
- Enhanced binding examples - Added one-way binding example alongside two-way in Data Binding section
- Clarified binding patterns - Corrected misleading statement about one-way binding requiring manual notifications
- Added UI integration - Dynamic canExecute section now shows complete ViewModel-to-UI flow
- Compacted Custom Type Equality - Streamlined section by removing redundant examples
- Added DisposableBag documentation - Utility for managing multiple disposable resources in ViewModels
Notes #
- Documentation-only release, no code changes
- Fully backward compatible
1.3.5 #
Documentation Overhaul & Regression Fix - Comprehensive README improvements and restores plain property binding support from v1.2.0.
๐ Bug Fixes #
- Fixed: Plain property getters with
onPropertyChanged()now properly trigger UI rebuilds- Issue: After v1.3.0, bindings using plain getters (e.g.,
selector: (vm) => vm.messagewheremessageis a regular property) stopped rebuilding - Solution: Restored ViewModel subscription fallback for plain properties while preserving v1.3.0's ObservableProperty tracking
- Impact: Both ObservableProperty and plain property patterns now work correctly
- Issue: After v1.3.0, bindings using plain getters (e.g.,
๐ Documentation #
- Improved README structure - Reduced from 1341 to ~520 lines (61% more compact)
- Added Quick Reference tables - Properties, Commands, and Widget types at a glance
- Added Common Patterns section - 4 practical copy-paste examples (form validation, list operations, loading states, dynamic canExecute)
- Added Performance benchmarks - Comparative analysis with Provider and Riverpod
- Updated positioning - "Simplicity over complexity" as core philosophy
- Unified root and package READMEs - Consistent high-quality documentation across GitHub and pub.dev
- Fixed Bind.viewModel2/3/4 documentation - Correctly described as multi-ViewModel binding
- Corrected
Bind.observerreferences toBind.viewModelin API documentation
๐งช Testing #
- Added 21 tests for plain property binding patterns
- Total: 543 tests passing (up from 522 in v1.3.0)
Notes #
- Fully backward compatible with v1.2.0 and v1.3.0
- No breaking changes
1.3.0 #
One-Way Binding Fix & Tuple Support Release - Critical bug fix for one-way binding with improved tuple handling.
This release fixes a critical bug where one-way bindings with .value access didn't trigger UI rebuilds, and adds comprehensive tuple binding support with clear documentation of limitations.
๐ Bug Fixes #
One-Way Binding Rebuild Issue (Critical)
- Fixed: One-way bindings now properly rebuild when ObservableProperty values change
- Issue: Selectors using
.value(e.g.,selector: (vm) => vm.property.value) didn't rebuild UI - Root Cause: One-way binding only subscribed to ViewModel.propertyChanged(), missing direct property changes
- Solution: Integrated DependencyTracker to automatically track all accessed ObservableProperty instances
- Impact: All
.valueaccesses in selectors now correctly trigger UI updates
- Issue: Selectors using
DependencyTracker Integration
- Enhanced: Bind widget now uses DependencyTracker for one-way bindings
- Automatically detects which ObservableProperty instances are accessed during selector evaluation
- Subscribes to all accessed properties for change notifications
- Re-tracks dependencies on each change for dynamic selector patterns
- Proper cleanup of all tracked listeners on disposal
๐งช Testing Enhancements #
Comprehensive Tuple Binding Tests
- Added 17 new tests for tuple selector patterns covering all scenarios
- โ
8 tests: Tuples with
.valueaccess (all working correctly) - โ
4 tests: Two-way binding without
.valuefor non-tuple cases - โ
4 tests: Tuples without
.value(correctly throwing TypeError to document limitation) - โ 1 test: Documentation explaining technical reasons for tuple limitations
- โ
8 tests: Tuples with
Memory Leak Prevention Tests
- Added 8 comprehensive tests validating proper disposal and cleanup
- Basic one-way binding disposal
- Multiple property cleanup
- Selector changes (switching between properties)
- List binding cleanup
- Stress testing (50 create/dispose cycles)
- Mixed one-way/two-way binding scenarios
- Rapid rebuild and disposal (20 cycles with 3 updates each)
- All tests validate no memory leaks after disposal
List Binding Tests
- Added 4 tests for one-way binding with list values
- Deep equality validation with list modifications
- Shallow equality with new list instances
- Proper understanding of deepEquality parameter behavior
๐ Documentation Improvements #
Tuple Binding Patterns
- Documented valid tuple patterns: All tuple items must use
.value// โ VALID Bind<VM, (int, String)>( selector: (vm) => (vm.counter.value, vm.message.value), builder: (context, tuple, update) => ... ) // โ INVALID - Will throw TypeError Bind<VM, (int, String)>( selector: (vm) => (vm.counter, vm.message), // ObservableProperty instances builder: (context, tuple, update) => ... )
Technical Limitation Explained
- Why tuples don't support ObservableProperty unwrapping:
- Automatic unwrapping only works for top-level return values
- Tuples with ObservableProperty instances fail type casting:
_selected as TValue - Supporting this would require runtime type introspection and recursive unwrapping
- Clear error messages guide developers to correct usage
๐ฏ Test Suite Status #
- Total Tests: 505 tests (up from 497)
- +8 tests for tuple binding scenarios
- +8 tests for memory leak prevention
- +4 tests for list binding patterns
- All Tests Passing: 100% pass rate
๐ฆ Breaking Changes #
None - This is a backward-compatible bug fix release.
๐ก Migration Guide #
No migration needed. This release fixes existing functionality without API changes. If you were experiencing issues with one-way bindings not updating, they will now work correctly.
1.2.0 #
Enhanced Dependency Tracking Release - Advanced lazy builder support and performance optimizations.
This release introduces intelligent dependency tracking for deferred callbacks (ListView.builder) and significantly improves rebuild performance for Bind.viewModel API.
๐ New Features #
Enhanced Dependency Tracker
- Lazy builder support - Automatic tracking for deferred callbacks like
ListView.builder- InheritedWidget-based fallback enables
itemBuilderproperty access detection - Stack-based primary tracking with context-based fallback for deferred execution
- Zero-configuration - works automatically with all lazy builders
- InheritedWidget-based fallback enables
โก Performance Improvements #
Benchmark Results (5-run average)
- Memory Management: Fastest cleanup (100% baseline, 0.5% faster than Riverpod, 6.7% faster than Provider)
- Selective Rebuilds: 22.6-34% faster than competitors
- Auto-binding: 0.9-3.9% faster with 100% rebuild efficiency
- Widget Performance: Within 1.8% of fastest framework
๐ฏ Key Achievements #
- 3 Gold Medals in memory management, selective rebuilds, and auto-binding
- 100% Rebuild Efficiency - Only framework achieving perfect selectivity vs 33% for Provider/Riverpod
- Reliable Results - Stable averages with engine warm-up for consistent measurements
๐ฆ Breaking Changes #
None - This is a backward-compatible enhancement release.
1.1.2 #
Documentation & Testing Release - Enhanced documentation clarity and comprehensive deep nesting test coverage.
This release focuses on improving developer experience through clearer documentation and ensuring robust FairyScope behavior with deep widget tree hierarchies.
๐ Documentation Improvements #
Enhanced Code Examples
- Disposer naming convention updated across all examples
- Renamed from
_disposeListener,_disposePropertyListenerto clearer names disposePropertyChangesfor property change subscriptionsdisposeCommandChangesfor command canExecute subscriptions- More self-documenting and consistent across the codebase
- Renamed from
Fixed "Without ComputedProperty" Example
- Added proper disposer captures to the manual implementation example
- Shows complete lifecycle management including dispose() override
- Makes the comparison with ComputedProperty even more compelling (28 lines vs 10 lines)
๐งช Testing Enhancements #
Deep Nesting Test Coverage
- Added 4-level FairyScope nesting test to ensure robust hierarchical dependency resolution
- Tests ViewModel access from first scope in fourth nested scope (3 levels up)
- Verifies all intermediate scope traversals work correctly
- Validates identity preservation across deep scope hierarchies
- 443 tests passing (up from 442)
๐ฆ Version Bump #
- Updated version to 1.1.2 across all package files
- No breaking changes - fully backward compatible
1.1.1 #
Memory Leak Prevention Release - Critical fix for ComputedProperty memory leaks with required parent parameter.
This release addresses a critical memory leak in ComputedProperty and introduces a cleaner, more explicit API that makes memory safety impossible to miss.
๐ Memory Leak Fix #
ComputedProperty Required Parent Parameter
- Parent parameter is now required and positional for
ComputedProperty- Before:
ComputedProperty<T>(compute, dependencies)- optional parent, easy to forget - After:
ComputedProperty<T>(compute, dependencies, this)- required parent, impossible to forget - Prevents memory leaks by ensuring ComputedProperty is always auto-disposed
- Cleaner, more concise syntax with positional parameter
- Compiler enforces correct usage - no runtime surprises
- Before:
Why This Matters
ComputedProperty creates circular references with its dependencies:
- ComputedProperty โ dependencies (strong reference)
- dependencies._listeners โ ComputedProperty._onDependencyChanged (strong reference back)
- Without disposal, this cycle prevents garbage collection
The required parent parameter ensures:
- โ Deterministic disposal - no magic, no finalizers needed
- โ Zero memory leaks - impossible to forget disposal
- โ Explicit is better than implicit - follows Dart/Flutter philosophy
- โ Compile-time safety - errors caught at compile time, not runtime
๐ง API Improvements #
Simplified ComputedProperty API
class UserViewModel extends ObservableObject {
final firstName = ObservableProperty<String>('John');
final lastName = ObservableProperty<String>('Doe');
// New: Required parent parameter (positional)
late final fullName = ComputedProperty<String>(
() => '${firstName.value} ${lastName.value}',
[firstName, lastName],
this, // Required - automatic disposal when ViewModel is disposed
);
}
๐ Documentation Updates #
- Updated all examples to show required parent parameter
- Enhanced ComputedProperty documentation with memory leak prevention
- Updated README with new syntax
- Clarified when parent parameter is needed
๐งช Testing #
- 442 tests passing (unchanged)
- Updated 6 parent-child disposal tests
- Removed tests for optional parent behavior
- Added test for post-disposal protection
๐ก Migration Guide #
Update all ComputedProperty instances to include the parent parameter:
// Before (v1.1.0)
late final total = ComputedProperty<double>(
() => subtotal.value + tax.value,
[subtotal, tax],
);
// After (v1.1.1)
late final total = ComputedProperty<double>(
() => subtotal.value + tax.value,
[subtotal, tax],
this, // Add parent parameter
);
This change applies to all ComputedProperty instances in your ViewModels. The compiler will catch any missing parameters, making migration straightforward.
1.1.0 #
Lifecycle & Disposal Management Release - Enhanced disposal safety, improved error handling, and better documentation.
This release focuses on robustness and developer experience with comprehensive improvements to lifecycle management across the framework. All components now provide clearer disposal semantics and better error messages.
๐ Lifecycle & Disposal Improvements #
Enhanced Disposal Safety
- ObservableObject now properly extends
ObservableNodewith improved lifecycle management- Clearer separation between internal API and public MVVM-style API
clearListeners()method added toObservableNodefor manual listener cleanup- Better disposal state tracking prevents operations on disposed objects
- Enhanced error messages when attempting to use disposed ViewModels
FairyLocator Disposal Checks
- Disposed ViewModel Detection:
FairyLocator.get<T>()now checks if ViewModels are disposed before returning- Throws informative
StateErrorwith guidance when accessing disposed ViewModels - Provides clear troubleshooting steps in error message
- Recommends unregistering ViewModels after manual disposal
- Helps catch lifecycle bugs early in development
- Throws informative
Example error message:
StateError: ViewModel of type MyViewModel has been disposed and cannot be accessed.
This usually happens when:
1. ViewModel was manually disposed but not unregistered
2. App is shutting down
Consider unregistering disposed ViewModels:
vm.dispose();
FairyLocator.instance.unregister<MyViewModel>();
FairyScope Error Handling
- Improved ViewModel Registration: Enhanced error handling in
FairyScopeData- Better tracking of owned ViewModels using List-based storage
- Clearer error messages when ViewModel registration fails
- Improved retrieval logic with better null safety
- More robust disposal of scoped ViewModels
Disposable Mixin Refinement
- Consolidated Disposal Logic: Streamlined
Disposablemixin implementation- Simplified state management with single
_isDisposedflag throwIfDisposed()provides consistent disposal checks- Better documentation with corrected class names in examples
- Cleaner code with reduced redundancy
- Simplified state management with single
๐ Documentation Enhancements #
ComputedProperty Documentation
- Comprehensive Documentation: Dramatically improved docs showing real-world value
- "Why You'll Love It" section with before/after comparison
- Real-world examples: Shopping cart, form validation, business logic
- Key benefits highlighted with clear use cases
- Performance notes explaining caching and optimization
- Shows how ComputedProperty eliminates manual synchronization headaches
Code Comments
- Slimmed Down: Reduced verbose code comments while maintaining clarity
- Follows Dart documentation standards
- Focuses on essential information with concise examples
- Links to comprehensive README for detailed examples
- Better balance between inline docs and external documentation
๐งช Testing #
- 436 tests passing (up from 401)
- Added 35+ new disposal and lifecycle tests:
- FairyScopeData registration, retrieval, and disposal tests
- FairyLocator disposal check tests
- ObservableObject disposal state validation
- Enhanced error handling tests
- Comprehensive lifecycle edge case coverage
๐ง API Improvements #
ObservableNode
- Added
clearListeners()method for explicit listener cleanup - Better error reporting during listener notification
- Consistent API across all observable types
Error Messages
- More informative error messages across the framework
- Actionable guidance in StateError exceptions
- Better troubleshooting information for common issues
๐ฆ Breaking Changes #
None - This is a backward-compatible enhancement release.
๐ฏ Migration Guide #
No migration needed. All changes are internal improvements and additions that enhance existing functionality without breaking existing code.
๐ก Developer Experience #
This release significantly improves the debugging experience:
- Disposed ViewModels are caught early with clear error messages
- Better lifecycle tracking helps prevent memory leaks
- Improved documentation makes ComputedProperty's value crystal clear
- More comprehensive tests ensure reliability
1.0.0 ๐ #
Stable Release - A lightweight MVVM framework for Flutter with strongly-typed, reactive data binding.
This release consolidates all improvements from RC builds (rc.1, rc.2, rc.3) into a stable production-ready package.
โจ New Features #
Async Command Execution State Tracking
isRunningproperty added to async commands for automatic execution state trackingAsyncRelayCommand.isRunning: Tracks execution state (true while running, false otherwise)AsyncRelayCommandWithParam<T>.isRunning: Same behavior for parameterized async commands- Automatic concurrent execution prevention:
canExecutereturnsfalsewhileisRunningistrue - Eliminates need for manual loading state management
- Prevents double-click bugs automatically
- Enables easy loading indicators in UI
Command Widget API Enhancement
- 4th parameter
isRunningadded to all Command builder signatures for consistencyCommand<TViewModel>: Builder now receivesisRunning(alwaysfalsefor sync commands)Command.param<TViewModel, TParam>: Builder now receivesisRunning(alwaysfalsefor sync commands)- Async commands return actual
isRunningstate from the command - Consistent API across all command types
Overlay ViewModel Bridging
FairyBridge: New widget to bridge ViewModels to overlay widget trees- Solves the problem of dialogs, bottom sheets, and menus creating separate widget trees
- Captures parent context's FairyScope and makes it available to overlay
- Enables
BindandCommandwidgets to work seamlessly in overlays - Gracefully falls back to FairyLocator if no FairyScope found
UI Widgets API Enhancement
Bind.viewModel<TViewModel>: Auto-tracking data binding for multiple properties- Eliminates need for manual selectors when displaying multiple properties
- Automatically tracks all accessed properties and rebuilds only when they change
- Achieves superior selective rebuild efficiency (100% accuracy)
- 4-10% faster than competitors while maintaining perfect selectivity
Command.param<TViewModel, TParam>: Factory constructor for parameterized commands- Provides consistent API alongside
Command<TViewModel> - Simplifies parameterized command binding in UI
- Completes the "Learn just 2 widgets" philosophy
- Provides consistent API alongside
Recursive Deep Equality for Collections
- Built-in recursive deep equality for all collection types without external dependencies
- Automatically handles arbitrary nesting depth:
List<Map<String, List<int>>> - Works with
List,Map,Set, andIterableat any level - Custom types use their
==operator when nested in collections - Zero configuration needed - deep equality enabled by default
- Automatically handles arbitrary nesting depth:
Equalsutility class for custom equality implementationsEquals.deepCollectionEquals()- Recursive equality for any collection typeEquals.deepCollectionHash()- Recursive hash code generation- Collection-specific methods:
listEquals,mapEquals,setEquals - Hash methods:
listHash,mapHash,setHash
ObservableProperty.deepEqualityparameter (default:true)- Primitive types use
==, collections use deep equality automatically - Override
==operator is optional for custom types
- Primitive types use
๐ Breaking Changes #
Command.param Parameter Type Change
- BREAKING:
Command.paramparameter changed from staticTParamto functionTParam Function()- Reason: Enables reactive parameter evaluation on rebuild
- Before:
parameter: todoId, - After:
parameter: () => todoId, - For reactive controller values, wrap with
ValueListenableBuilder
Command Builder Signature Update
- BREAKING: All Command builder signatures now include 4th
isRunningparameter- Before:
builder: (context, execute, canExecute) { ... } - After:
builder: (context, execute, canExecute, isRunning) { ... } - Applies to both
Command<TViewModel>andCommand.param<TViewModel, TParam> isRunningis always present but only meaningful for async commands (false for sync)
- Before:
Removed Extensions
- BREAKING: Removed
ObservableObjectExtensionsfor creating properties/commands- Before:
final counter = observableProperty<int>(0); - After:
final counter = ObservableProperty<int>(0); - Reason: Direct type usage is clearer, more discoverable, and follows Dart conventions
- Replace all lowercase helpers (
observableProperty,computedProperty,relayCommand, etc.) with direct constructors
- Before:
Command Constructor Changes
- BREAKING: Removed
parentparameter from all command constructors- Before:
RelayCommand(execute, parent: this, canExecute: ...) - After:
RelayCommand(execute, canExecute: ...) - Reason: Auto-disposal makes parent tracking unnecessary
- Before:
๐ Performance Improvements #
Comprehensive benchmarks show exceptional performance achievements:
- ๐ฅ Memory Management: Highly optimized cleanup and disposal system
- ๐ฅ Selective Rebuilds: Exceptional performance with explicit
Bindselectors - ๐ฅ Auto-Binding Performance:
Bind.viewModeldelivers superior speed while maintaining perfect selectivity - Unique Achievement: 100% rebuild efficiency with
Bind.viewModel- only rebuilds when accessed properties change - Deep equality optimized with fast-path
identical()checks and efficient recursive comparison
๐ Documentation #
- Comprehensive "Learn just 2 widgets" positioning (
BindandCommand) - Added examples for all new features:
isRunning,FairyBridge,Bind.viewModel,Command.param - Updated all Command widget examples with 4th
isRunningparameter - Added ValueListenableBuilder pattern for reactive parameters
- Deep equality usage examples for collections and custom types
- Enhanced best practices section with memory leak warnings
- Complete API reference in llms.txt
- Added benchmark results demonstrating performance leadership
๐งช Testing #
- 401 tests passing with comprehensive coverage
- Tests for async command execution state tracking
- Tests for
FairyBridgewidget with overlay scenarios - Tests for
Bind.viewModelauto-tracking functionality - Tests for
Command.paramfactory constructor - Tests for recursive deep equality (43 comprehensive tests)
- All breaking changes validated with updated tests
๐ฏ Framework Philosophy #
- "Learn just 2 widgets":
Bindfor data,Commandfor actions - No code generation: Zero build_runner dependency
- Type-safe: Strong typing throughout the API
- Automatic disposal: No memory leaks with proper patterns
- Zero external dependencies: Only Flutter SDK required
- Built-in deep equality: No external packages needed
1.0.0-rc.3 #
โจ New Features #
Async Command Execution State Tracking
isRunningproperty added to async commands for automatic execution state trackingAsyncRelayCommand.isRunning: Tracks execution state (true while running, false otherwise)AsyncRelayCommandWithParam<T>.isRunning: Same behavior for parameterized async commands- Automatic concurrent execution prevention:
canExecutereturnsfalsewhileisRunningistrue - Eliminates need for manual loading state management
- Prevents double-click bugs automatically
- Enables easy loading indicators in UI
Command Widget API Enhancement
- 4th parameter
isRunningadded to all Command builder signatures for consistencyCommand<TViewModel>: Builder now receivesisRunning(alwaysfalsefor sync commands)Command.param<TViewModel, TParam>: Builder now receivesisRunning(alwaysfalsefor sync commands)- Async commands return actual
isRunningstate from the command - Consistent API across all command types
// Async command with loading indicator
Command<DataViewModel>(
command: (vm) => vm.fetchCommand,
builder: (context, execute, canExecute, isRunning) {
if (isRunning) return CircularProgressIndicator();
return ElevatedButton(
onPressed: execute,
child: Text('Fetch Data'),
);
},
)
// Sync command (isRunning always false)
Command<TodoViewModel>(
command: (vm) => vm.deleteCommand,
builder: (context, execute, canExecute, isRunning) {
return IconButton(
onPressed: canExecute ? execute : null,
icon: Icon(Icons.delete),
);
},
)
Overlay ViewModel Bridging
FairyBridge: New widget to bridge ViewModels to overlay widget trees- Solves the problem of dialogs, bottom sheets, and menus creating separate widget trees
- Captures parent context's FairyScope and makes it available to overlay
- Enables
BindandCommandwidgets to work seamlessly in overlays - Gracefully falls back to FairyLocator if no FairyScope found
void _showDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => FairyBridge(
context: context, // Makes parent FairyScope available
child: AlertDialog(
actions: [
Command<MyViewModel>(
command: (vm) => vm.saveCommand,
builder: (ctx, execute, canExecute, isRunning) =>
TextButton(onPressed: execute, child: Text('Save')),
),
],
),
),
);
}
๐ Breaking Changes #
Command.param Parameter Type Change
- BREAKING:
Command.paramparameter changed from staticTParamto functionTParam Function()- Reason: Enables reactive parameter evaluation on rebuild
- Before:
parameter: todoId, - After:
parameter: () => todoId, - For reactive controller values, wrap with
ValueListenableBuilder:
// Before (static parameter - doesn't react to changes)
Command.param<TodoViewModel, String>(
parameter: controller.text, // Static value
builder: (context, execute, canExecute) { ... },
)
// After (reactive parameter)
ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) {
return Command.param<TodoViewModel, String>(
parameter: () => value.text, // Reactive to text changes
builder: (context, execute, canExecute, isRunning) { ... },
);
},
)
Command Builder Signature Update
- BREAKING: All Command builder signatures now include 4th
isRunningparameter- Before:
builder: (context, execute, canExecute) { ... } - After:
builder: (context, execute, canExecute, isRunning) { ... } - Applies to both
Command<TViewModel>andCommand.param<TViewModel, TParam> isRunningis always present but only meaningful for async commands (false for sync)
- Before:
๐งช Testing #
- 401 tests passing
- Updated 5 tests for new concurrent execution prevention behavior
- Added tests for
isRunningstate tracking - Added tests for Command widget
isRunningparameter
๐ Documentation
- Added comprehensive examples for
isRunningusage - Added
FairyBridgewidget documentation and examples - Updated all Command widget examples with 4th parameter
- Added ValueListenableBuilder pattern for reactive parameters
- Updated llms.txt with new API surface
1.0.0-rc.2 #
โจ New Features #
Recursive Deep Equality for Collections
- Built-in recursive deep equality for all collection types without external dependencies
- Automatically handles arbitrary nesting depth:
List<Map<String, List<int>>> - Works with
List,Map,Set, andIterableat any level - Custom types use their
==operator when nested in collections - Zero configuration needed - deep equality enabled by default
- Automatically handles arbitrary nesting depth:
Equals Utility Class
Equals.deepCollectionEquals(Object? e1, Object? e2): Recursive equality for any collection typeEquals.deepCollectionHash(Object? o): Recursive hash code generation- Collection-specific methods:
listEquals,mapEquals,setEqualswith deep comparison - Hash methods:
listHash,mapHash,setHashfor consistent hash codes Equals.deepEquals<T>(): Factory method forObservablePropertyequality parameter
๐ง API Enhancements #
ObservableProperty Deep Equality
deepEquality: boolparameter (default:true) for automatic collection comparison- Primitive types:
ObservableProperty<int>(0)- uses== - Collections:
ObservableProperty<List<int>>([1, 2, 3])- uses deep equality - Custom types: Override
==operator is optional (only needed for value-based equality)
- Primitive types:
๐ Developer Experience #
Optional Equality Override
- Simplified workflow: No need to override
==for custom types with collections- Collections are compared deeply automatically
- Override
==only if you want value-based equality instead of reference equality - Use
Equalsutilities in custom==implementations when overriding
// Works automatically without custom ==
final todos = ObservableProperty<List<String>>(['Task 1', 'Task 2']);
todos.value = ['Task 1', 'Task 2']; // โ
No rebuild - deep equality
// Custom type - override == is optional
class Project {
final String name;
final List<String> tasks;
// OPTIONAL: Override for value-based equality
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Project &&
name == other.name &&
Equals.listEquals(tasks, other.tasks);
}
๐งช Testing #
- 387 tests passing (up from 344)
- Added 43 comprehensive deep equality tests:
- Simple collections (List, Map, Set)
- Nested structures with 2-3 levels of nesting
- Mixed nested structures:
List<Map<String, List<int>>> - Sets with nested lists
- Custom types with and without equality overrides
- Nested custom types containing collections
- Performance and memory tests
๐ Performance #
- Deep equality optimized with fast-path
identical()checks - Recursive comparison with efficient callback pattern
- No performance impact on primitive types
- Benchmarks show excellent overall performance maintained
1.0.0-rc.1 #
๐ Major Release Candidate #
This release represents a major milestone with significant API improvements, enhanced performance, and comprehensive testing.
โจ New Features #
UI Widgets API Enhancement
Bind.viewModel<TViewModel>: New factory constructor for automatic property tracking- Eliminates need for manual selectors when displaying multiple properties
- Automatically tracks all accessed properties and rebuilds only when they change
- Achieves great selective rebuild efficiency over other state management solutions
- 4-10% faster than competitors while maintaining perfect selectivity
Command.param<TViewModel, TParam>: New factory constructor for parameterized commands- Provides consistent API alongside
Command<TViewModel> - Simplifies parameterized command binding in UI
- Completes the "2 widgets" framework positioning
- Provides consistent API alongside
๐ Breaking Changes #
Removed Extensions
- BREAKING: Removed
ObservableObjectExtensionsfor creating properties/commands- Before (Properties):
final counter = observableProperty<int>(0); - After (Properties):
final counter = ObservableProperty<int>(0); - Before (Commands):
late final saveCommand = relayCommand(_save); - After (Commands):
late final saveCommand = RelayCommand(_save); - Reason: Direct type usage is clearer, more discoverable, and follows Dart conventions
- Migration: Replace all
observableProperty<T>()withObservableProperty<T>() - Migration: Replace all
computedProperty<T>()withComputedProperty<T>() - Migration: Replace all command helpers (
relayCommand,asyncRelayCommand, etc.) with direct constructors (RelayCommand,AsyncRelayCommand, etc.)
- Before (Properties):
Command Constructor Changes
- BREAKING: Removed
parentparameter from all command constructors- Before:
RelayCommand(execute, parent: this, canExecute: ...) - After:
RelayCommand(execute, canExecute: ...) - Reason: Auto-disposal makes parent tracking unnecessary
- Migration: Remove
parent: thisfrom all command instantiations
- Before:
๐ Performance Improvements #
Comprehensive benchmarks show significant performance achievements:
- ๐ฅ Memory Management: Highly optimized cleanup and disposal system
- ๐ฅ Selective Rebuilds: Exceptional performance with explicit
Bindselectors - ๐ฅ Auto-tracking Performance:
Bind.viewModeldelivers superior speed while maintaining perfect selectivity - Unique Achievement: 100% rebuild efficiency with
Bind.viewModel- only rebuilds when accessed properties change
๐ Documentation Improvements #
- Updated all examples to use direct type constructors
- Added comprehensive
Bind.viewModelusage examples - Added
Command.paramexamples throughout documentation - "2 widgets" framework (Learn just
BindandCommand) - Enhanced best practices section with memory leak warnings
- Added benchmark results to main README
๐งช Testing #
- 344 tests passing (up from 299)
- Added comprehensive tests for new
Bind.viewModelfunctionality - Added tests for
Command.paramfactory constructor - All existing functionality validated with updated API
๐ฆ What's Next #
The 1.0.0 stable release is planned after community feedback on this RC. Please report any issues or suggestions!
0.5.0+2 #
- Improved documentation and fixed minor typos.
0.5.0+1 #
- Improved documentation and fixed minor typos.
0.5.0 #
Initial release of Fairy - A lightweight MVVM framework for Flutter.
Features #
Core Primitives
- ObservableObject: Base ViewModel class with clean MVVM API
onPropertyChanged()for manual notificationspropertyChanged(listener)method returning disposer functionsetProperty<T>()helper for batch updates with change detection- Auto-disposal: Properties created during construction are automatically disposed
- ObservableProperty: Strongly-typed reactive properties
- Automatic change notifications with custom equality support
propertyChanged(listener)for subscribing to property changes (returns disposer)- Auto-disposal when parent ObservableObject is disposed
- ComputedProperty: Derived properties with automatic dependency tracking
- Read-only computed values based on other properties
- Automatic updates when dependencies change
- Auto-disposal when parent ObservableObject is disposed
Commands
- RelayCommand: Synchronous commands with optional
canExecutevalidation - AsyncRelayCommand: Asynchronous commands with automatic
isRunningstate - RelayCommandWithParam: Parameterized commands for actions requiring input
- AsyncRelayCommandWithParam: Async parameterized commands
- All commands use named parameters:
execute:,canExecute:,parent: notifyCanExecuteChanged()method to re-evaluatecanExecuteconditionscanExecuteChanged(listener)method for subscribing tocanExecutechanges (returns disposer function)
Dependency Injection
- FairyLocator: Global singleton registry for app-wide services
registerSingleton<T>()for singleton registrationregisterFactory<T>()for factory registrationget<T>()for service resolutionunregister<T>()for cleanup
- FairyScope: Widget-scoped DI with automatic disposal
- Scoped ViewModels auto-disposed when widget tree is removed
- Supports both
createandinstanceparameters
- Fairy (ViewModelLocator): Unified resolution checking scope โ global โ exception
Fairy.of<T>(context): Idiomatic Flutter API for resolving ViewModels (similar toProvider.of,Theme.of)Fairy.maybeOf<T>(context): Optional resolution returningnullif not found
UI Binding
- Bind<TViewModel, TValue>: Automatic one-way/two-way binding detection
- Returns
ObservableProperty<T>โ two-way binding withupdatecallback - Returns raw
Tโ one-way binding (read-only) - Type-safe selector/builder contracts
- Returns
- Command: Command binding with automatic
canExecutereactivity - CommandWithParam<TViewModel, TParam>: Parameterized command binding
Auto-Disposal System
- Parent Parameter: Properties, commands, and computed properties accept optional
parentparameter- Pass
parent: thisin constructor to enable automatic disposal - Children are registered with parent and disposed automatically
- Debug warnings shown when parent is not provided
- Nested ObservableObject instances must be disposed manually
- Pass
Memory Management #
- Auto-disposal: ObservableProperty, ComputedProperty, and Commands automatically disposed when
parentparameter is provided - Nested ViewModels Exception: Nested ObservableObject instances require manual disposal
- Manual Listeners: Always capture disposer from
propertyChanged()andcanExecuteChanged()calls to avoid memory leaks - Use
BindandCommandwidgets for UI (automatic lifecycle management)
Best Practices #
- โ ๏ธ Memory Leak Prevention: Always capture disposer from manual
propertyChanged()andcanExecuteChanged()calls - Pass
parent: thisto properties, commands, and computed properties for auto-disposal - Nested ViewModels require explicit manual disposal
- Call
command.notifyCanExecuteChanged()whencanExecutedependencies change - Use
command.canExecuteChanged(listener)to listen tocanExecutestate changes - Selectors must return stable property references
- Use
FairyScopefor page-level ViewModels (handles disposal automatically) - Use named parameters for commands:
execute:,canExecute:,parent:
Documentation #
- Comprehensive README with quick start guide
- Auto-disposal explanation and migration patterns
- Complete API reference with examples
- Example app demonstrating MVVM patterns
Testing #
- Comprehensive unit and widget tests with 100% passing rate
- Tests cover all core primitives, DI patterns, UI bindings, and auto-disposal
- Test structure mirrors library organization