declare 1.1.0
declare: ^1.1.0 copied to clipboard
A lightweight, SwiftUI-inspired Prop management library for Flutter. Powered by Prop, ViewModel, and Published.
๐ฆ Declare #
SwiftUI-inspired reactive state management for Flutter โ powered by Prop<T>, Published<T>, Computed<T>, and ViewModel concepts.
Simple. Lightweight. Declarative. No external dependencies. Auto-reactive โ no more Observer widgets needed!
โจ Features #
โ
Auto-reactive UI โ access viewModel.count.value directly, rebuilds automatically
โ
Minimal syntax inspired by SwiftUI and modern reactive frameworks
โ
Prop<T> โ reactive local state with automatic notification
โ
Published<T> โ nullable reactive state that notifies parent ViewModel
โ
Computed<T> โ derived reactive values based on dependencies
โ
ViewModel โ lifecycle-aware logic container managing reactive states
โ
Declare<T> โ widget with automatic dependency tracking and rebuilding
โ
Pure Dart and Flutter โ no third-party dependencies
โ
Clean MVVM architecture with excellent developer experience
๐ What's New in Auto-Reactive Mode #
// โ
New way - Auto-reactive!
Text('Count: ${viewModel.count.value}') // Rebuilds automatically!
๐งฑ Getting Started #
Add declare to your pubspec.yaml:
dependencies:
declare: ^1.1.0
Basic Example - Auto-Reactive Counter #
class CounterViewModel extends ViewModel {
late final count = state(0);
late final doubleCount = computed(() => count.value * 2, [count]);
late final message = state('Hello World');
void increment() => count.value++;
void decrement() => count.value--;
void updateMessage(String newMessage) => message.value = newMessage;
@override
void onInit() {
print('CounterViewModel initialized');
}
@override
void onDispose() {
print('CounterViewModel disposed');
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Declare<CounterViewModel>(
create: () => CounterViewModel(),
builder: (context, viewModel) {
return Scaffold(
appBar: AppBar(title: Text('Auto-Reactive Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// These automatically rebuild when values change! ๐
Text('Count: ${viewModel.count.value}'),
Text('Double: ${viewModel.doubleCount.value}'),
Text('Message: ${viewModel.message.value}'),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: viewModel.decrement,
child: Text('-'),
),
ElevatedButton(
onPressed: viewModel.increment,
child: Text('+'),
),
],
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => viewModel.updateMessage('Updated!'),
child: Text('Update Message'),
),
],
),
),
);
},
);
}
}
Advanced Example - Shopping Cart #
class Product {
final String name;
final double price;
Product(this.name, this.price);
}
class CartViewModel extends ViewModel {
late final items = state<List<Product>>([]);
late final total = computed(() =>
items.value.fold(0.0, (sum, item) => sum + item.price),
[items]
);
late final itemCount = computed(() => items.value.length, [items]);
void addItem(Product product) {
items.value = [...items.value, product];
}
void removeItem(Product product) {
items.value = items.value.where((item) => item != product).toList();
}
void clearCart() {
items.value = [];
}
}
class CartView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Declare<CartViewModel>(
create: () => CartViewModel(),
builder: (context, viewModel) {
return Scaffold(
appBar: AppBar(
title: Text('Cart (${viewModel.itemCount.value})'),
),
body: Column(
children: [
Text('Total: \$${viewModel.total.value.toStringAsFixed(2)}'),
Expanded(
child: ListView.builder(
itemCount: viewModel.items.value.length,
itemBuilder: (context, index) {
final item = viewModel.items.value[index];
return ListTile(
title: Text(item.name),
subtitle: Text('\$${item.price}'),
trailing: IconButton(
icon: Icon(Icons.remove),
onPressed: () => viewModel.removeItem(item),
),
);
},
),
),
],
),
);
},
);
}
}
๐ API Overview #
Core Classes #
Prop<T> โ Reactive wrapper around a value with update and transform methods
final count = Prop<int>(0);
count.value = 5; // Automatically notifies listeners
count.increment(); // Built-in helper for numbers
count.transform((current) => current * 2); // Transform current value
Published<T> โ Nullable reactive state that notifies parent ViewModel
final user = Published<User>(); // Starts as null
user.value = User('John');
user.clear(); // Set back to null
print(user.hasValue); // Check if value exists
Computed<T> โ Derived reactive value computed from dependencies
final fullName = computed(() => '${firstName.value} ${lastName.value}', [firstName, lastName]);
// Automatically updates when firstName or lastName changes
ViewModel โ Base class managing reactive states with lifecycle hooks
class MyViewModel extends ViewModel {
late final count = state(0); // Creates Prop<int>
late final user = published<User>(); // Creates Published<User>
late final doubleCount = computed(() => count.value * 2, [count]);
@override
void onInit() { /* Called when ViewModel is created */ }
@override
void onDispose() { /* Called when ViewModel is disposed */ }
}
Declare<T> โ Widget that creates and manages ViewModel lifecycle
Declare<MyViewModel>(
create: () => MyViewModel(),
builder: (context, viewModel) {
return Text('Count: ${viewModel.count.value}'); // Auto-reactive!
},
);
Extensions #
Reactive Extensions โ Chain operations on any ValueListenable<T>
final doubled = count.map((value) => value * 2);
final combined = count.combine(name, (c, n) => '$n: $c');
Prop Extensions โ Convenient methods for common operations
count.increment(); // For numbers
count.decrement(); // For numbers
isVisible.toggle(); // For booleans
List Extensions โ Easy list manipulation
final items = state<List<String>>([]);
items.add('New item');
items.remove('Old item');
items.clear();
items.updateAt(0, 'Updated item');
๐ How Auto-Reactivity Works #
- Dependency Tracking: When you access
viewModel.count.valueduring a build, Declare automatically tracks this dependency - Change Detection: When
count.valuechanges, all widgets that accessed it are marked for rebuild - Efficient Updates: Only the specific
Declarewidgets that depend on changed values are rebuilt - No Manual Subscriptions: No need to wrap values in
Observerwidgets or manually manage listeners
โ๏ธ Lifecycle #
ViewModel.onInit()is called once whenDeclareinitializesViewModel.onDispose()is called whenDeclaredisposes- Reactive states automatically notify their parent ViewModel
- Auto-tracking subscribes to accessed values during build phase
- All reactive resources are automatically cleaned up on disposal
๐ฏ Best Practices #
โ Do #
// Use direct value access in Declare
Text('Count: ${viewModel.count.value}')
// Create computed values for derived state
late final total = computed(() => items.value.fold(0, (a, b) => a + b.price), [items]);
// Use lifecycle hooks for initialization
@override
void onInit() {
loadUserData();
}
โ Don't #
// Don't forget to use .value when accessing
Text('Count: ${viewModel.count}') // Missing .value!
// Don't mutate state directly in build method
Text('Count: ${viewModel.count.value++}') // Side effect in build!
๐ Performance Benefits #
- Granular Updates: Only widgets that access changed values rebuild
- Automatic Optimization: No need to manually optimize with
ConsumerorSelectorpatterns - Minimal Overhead: Dependency tracking adds negligible performance cost
- Memory Efficient: Automatic cleanup prevents memory leaks
๐งช Testing #
ViewModels are easy to test since they're pure Dart classes:
void main() {
group('CounterViewModel', () {
late CounterViewModel viewModel;
setUp(() {
viewModel = CounterViewModel();
});
tearDown(() {
viewModel.dispose();
});
test('should increment count', () {
expect(viewModel.count.value, 0);
viewModel.increment();
expect(viewModel.count.value, 1);
});
test('should compute double count', () {
viewModel.count.value = 5;
expect(viewModel.doubleCount.value, 10);
});
});
}
๐จ Comparison with Other Solutions #
| Feature | Declare | Provider | Riverpod | Bloc |
|---|---|---|---|---|
| Auto-reactive | โ | โ | โ | โ |
| Boilerplate | Minimal | Medium | Low | High |
| Learning Curve | Easy | Easy | Medium | Hard |
| Performance | Excellent | Good | Excellent | Excellent |
| Testing | Easy | Medium | Easy | Easy |
| DevTools | Basic | Excellent | Excellent | Excellent |
๐ฎ Roadmap #
- โ DevTools integration for state inspection
- โ Async state management utilities
- โ State persistence helpers
- โ Time-travel debugging
- โ React DevTools-style state inspector
๐ค Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License #
This project is licensed under the MIT License.
Made with โค๏ธ for the Flutter community
Declare: Where reactive meets declarative ๐