A lightweight helper library that makes Flutter’s built-in state management easier to use.
Flutter's InheritedWidget and InheritedModel are powerful but boilerplate-heavy. Ease provides a modern, Riverpod-like Developer Experience (DX) directly on top of these native primitives.
It aims to be the "Standard Library" extension for Flutter state management—zero extra dependencies, native performance, and type safety out of the box.
If you've used Riverpod or Provider, Ease will feel familiar:
// context.counterViewModel => Watch (rebuilds widget)
// context.readCounterViewModel() => Read (no rebuild, for callbacks)
// context.selectCounterViewModel => Select (rebuilds only on specific field change)- Motivation
- Getting Started
- Core Concepts
- API Reference
- Advanced Usage
- DevTools Support
- Packages
- Examples
- Community & Support
- Contributing
Add the latest version to your pubspec.yaml:
dependencies:
ease_state_helper: ^0.3.0
ease_annotation: ^0.2.0
dev_dependencies:
ease_generator: ^0.3.0
build_runner: ^2.4.0import 'package:ease_annotation/ease_annotation.dart';
import 'package:ease_state_helper/ease_state_helper.dart';
part 'counter_view_model.ease.dart';
@ease
class CounterViewModel extends StateNotifier<int> {
CounterViewModel() : super(0);
void increment() => state++;
}dart run build_runner buildvoid main() {
runApp(
EaseScope(
providers: [(child) => CounterViewModelProvider(child: child)],
child: const MyApp(),
),
);
}Widget build(BuildContext context) {
final counter = context.counterViewModel;
return Text('${counter.state}');
}Base class for all ViewModels. Extends ChangeNotifier with a typed state property.
class CartViewModel extends StateNotifier<CartState> {
CartViewModel() : super(const CartState());
// Direct assignment - auto-notifies
void clear() => state = const CartState();
// Named action - better DevTools visibility
void addItem(Product product) {
setState(
state.copyWith(items: [...state.items, product]),
action: 'addItem',
);
}
// Functional update
void toggleLoading() {
update((current) => current.copyWith(isLoading: !current.isLoading));
}
}Use EaseScope to nest multiple providers:
void main() {
runApp(
EaseScope(
providers: [
(child) => CounterViewModelProvider(child: child),
(child) => CartViewModelProvider(child: child),
],
child: const MyApp(),
),
);
}Or nest them manually:
void main() {
runApp(
CounterViewModelProvider(
child: CartViewModelProvider(
child: const MyApp(),
),
),
);
}Marks a StateNotifier class for code generation.
// Global provider - included in $easeProviders
@ease
class AppViewModel extends StateNotifier<AppState> { ... }
// Local provider - manually placed in widget tree
@Ease(local: true)
class FormViewModel extends StateNotifier<FormState> { ... }For each ViewModel, these extensions are generated:
| Method | Subscribes | Rebuilds | Use Case |
|---|---|---|---|
context.myViewModel |
✅ Yes | On any change | Display state in UI |
context.readMyViewModel() |
❌ No | Never | Callbacks, event handlers |
context.selectMyViewModel((s) => s.field) |
✅ Partial | When selected value changes | Optimized rebuilds |
context.listenOnMyViewModel((prev, next) => ...) |
❌ No | Never | Side effects |
// Rebuilds entire widget when ANY state property changes
final cart = context.cartViewModel;
Text('Items: ${cart.state.items.length}');
Text('Total: \$${cart.state.total}');// Never rebuilds - use for callbacks and event handlers
ElevatedButton(
onPressed: () {
final cart = context.readCartViewModel();
cart.addItem(product);
},
child: const Text('Add to Cart'),
)// Only rebuilds when itemCount changes
final itemCount = context.selectCartViewModel((s) => s.itemCount);
// With custom equality for complex types
final items = context.selectCartViewModel(
(s) => s.items,
equals: (a, b) => listEquals(a, b),
);Use listenOnYourViewModel for context-aware side effects (snackbars, navigation). It automatically handles unmounting and cleanup.
@override
void initState() {
super.initState();
context.listenOnCartViewModel((prev, next) {
if (next.error != null && prev.error != next.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next.error!)),
);
}
});
}class CartState {
final List<CartItem> items;
final bool isLoading;
final String? error;
const CartState({
this.items = const [],
this.isLoading = false,
this.error,
});
// Computed properties
int get itemCount => items.fold(0, (sum, item) => sum + item.quantity);
double get subtotal => items.fold(0, (sum, item) => sum + item.total);
double get tax => subtotal * 0.1;
double get total => subtotal + tax;
CartState copyWith({
List<CartItem>? items,
bool? isLoading,
String? error,
}) {
return CartState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}For state that should be scoped to a specific part of the widget tree:
@Ease(local: true)
class FormViewModel extends StateNotifier<FormState> {
FormViewModel() : super(const FormState());
// ...
}
// Manually wrap the subtree that needs this state
class MyFormScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FormViewModelProvider(
child: const FormContent(),
);
}
}Add logging or other middleware to all state changes:
void main() {
StateNotifier.middleware = [
LoggingMiddleware(),
// Add custom middleware
];
runApp(
EaseScope(
providers: [
(child) => CounterViewModelProvider(child: child),
],
child: const MyApp(),
),
);
}Debug your Ease states in Flutter DevTools.
Currently, you can use the DevTools extension by adding it as a git dependency:
dev_dependencies:
ease_devtools_extension:
git:
url: https://github.com/y3l1n4ung/ease.git
path: packages/ease_devtools_extensionvoid main() {
initializeEaseDevTool(); // Enable DevTools integration
runApp(
EaseScope(
providers: [
(child) => CounterViewModelProvider(child: child),
],
child: const MyApp(),
),
);
}- State Inspector - View all registered states and current values
- History Timeline - Track state changes with timestamps
- Action Tracking - See what triggered each state change
- Filter Support - Filter history by state type
| Package | Description | Status |
|---|---|---|
| ease_state_helper | Core runtime library | |
| ease_annotation | @Ease() annotation |
|
| ease_generator | Code generator | |
| ease_devtools_extension | DevTools integration | ⏳ Coming Soon |
Ease is maintained as a monorepo using Melos.
| Path | Package | Description |
|---|---|---|
packages/ease_state_helper |
ease_state_helper | Core runtime library. |
packages/ease_annotation |
ease_annotation | Annotations for code generation. |
packages/ease_generator |
ease_generator | build_runner based code generator. |
packages/ease_devtools_extension |
ease_devtools_extension | Flutter DevTools extension. |
apps/example |
- | General examples and integration tests. |
apps/shopping_app |
- | Real-world example application. |
Check out the example apps in the repository:
| Example | Description |
|---|---|
| example | Comprehensive examples: Counter, Todo, Cart, Auth, Theme |
| shopping_app | Real-world e-commerce app with FakeStore API |
cd apps/example
flutter pub get
dart run build_runner build
flutter run- Need help? Check out our Support Guide.
- Found a bug? Open an issue.
- Want to chat? Join our Discord.
- Code of Conduct: Please read and follow our Code of Conduct.
For a no-code-generation workflow:
- Install Ease State Helper extension from VS Code marketplace
- Right-click folder → Ease: New ViewModel
- Enter name and state type
The extension generates both .dart and .ease.dart files without needing build_runner.
Contributions are welcome!
Here's how you can help:
- 🐛 Bug reports - Found an edge case or unexpected behavior? Open an issue
- 📖 Documentation - Improve guides, fix typos, or add code examples
- ✨ Features - Have an idea? Discuss it in an issue first, then submit a PR
- 🧪 Tests - Help improve test coverage
git clone https://github.com/y3l1n4ung/ease.git
cd ease
melos bootstrap
melos run test:allSee the Contributing Guide for detailed guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.