fairy 1.0.0
fairy: ^1.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.
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