Ease State Helper

A lightweight helper library that makes Flutter’s built-in state management easier to use.

pub package License: MIT Flutter Dart codecov Discord


Motivation

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.


The Model

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)

Table of Contents


Getting Started

1. Installation

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.0

2. Create a ViewModel

import '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++;
}

3. Generate & Register

dart run build_runner build
void main() {
  runApp(
    EaseScope(
      providers: [(child) => CounterViewModelProvider(child: child)],
      child: const MyApp(),
    ),
  );
}

4. Use

Widget build(BuildContext context) {
  final counter = context.counterViewModel;
  return Text('${counter.state}');
}

Core Concepts

StateNotifier<T>

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));
  }
}

Provider Nesting

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(),
      ),
    ),
  );
}

@ease Annotation

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> { ... }

API Reference

Context Extensions

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

Watch (Subscribe)

// Rebuilds entire widget when ANY state property changes
final cart = context.cartViewModel;
Text('Items: ${cart.state.items.length}');
Text('Total: \$${cart.state.total}');

Read (No Subscribe)

// Never rebuilds - use for callbacks and event handlers
ElevatedButton(
  onPressed: () {
    final cart = context.readCartViewModel();
    cart.addItem(product);
  },
  child: const Text('Add to Cart'),
)

Select (Partial Subscribe)

// 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),
);

Context-Safe Listeners (Side Effects)

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!)),
      );
    }
  });
}

Advanced Usage

Complex State with copyWith

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,
    );
  }
}

Local/Scoped Providers

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(),
    );
  }
}

Middleware

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(),
    ),
  );
}

DevTools Support

Debug your Ease states in Flutter DevTools.

Setup (Coming Soon to pub.dev)

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_extension
void main() {
  initializeEaseDevTool(); // Enable DevTools integration
  runApp(
    EaseScope(
      providers: [
        (child) => CounterViewModelProvider(child: child),
      ],
      child: const MyApp(),
    ),
  );
}

Features

  • 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

Packages

Package Description Status
ease_state_helper Core runtime library pub
ease_annotation @Ease() annotation pub
ease_generator Code generator pub
ease_devtools_extension DevTools integration ⏳ Coming Soon

Project Structure

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.

Examples

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

Running Examples

cd apps/example
flutter pub get
dart run build_runner build
flutter run

Community & Support


VS Code Extension

For a no-code-generation workflow:

  1. Install Ease State Helper extension from VS Code marketplace
  2. Right-click folder → Ease: New ViewModel
  3. Enter name and state type

The extension generates both .dart and .ease.dart files without needing build_runner.


Contributing

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

Development Setup

git clone https://github.com/y3l1n4ung/ease.git
cd ease
melos bootstrap
melos run test:all

See the Contributing Guide for detailed guidelines.


License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

ease_state_helper
Simple Flutter state management helper.