watchable_redux 1.2.0 copy "watchable_redux: ^1.2.0" to clipboard
watchable_redux: ^1.2.0 copied to clipboard

discontinued

Predictable state management for Flutter. Redux architecture with O(1) selector caching, memoized selectors, async middleware, and time-travel debugging. Built on Watchable.

Watchable Redux #

Predictable state management for Flutter. Built on Watchable.

// Define
final store = Store<int>(initialState: 0, reducer: counterReducer);

// Select with O(1) caching
store.select((s) => s).build((count) => Text('$count'));

// Update
store.dispatch(Increment());

Redux architecture with the simplicity of Watchable. Zero boilerplate, O(1) rebuilds.


Quick Start #

dependencies:
  watchable_redux: ^1.2.0
import 'package:watchable_redux/watchable_redux.dart';

// 1. Define Actions
class Increment extends ReduxAction {
  const Increment();
}

// 2. Define Reducer
int counterReducer(int state, ReduxAction action) {
  return switch (action) {
    Increment() => state + 1,
    _ => state,
  };
}

// 3. Create Store
final store = Store<int>(
  initialState: 0,
  reducer: counterReducer,
);

// 4. Build UI
class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: store.select((s) => s).build(
          (count) => Text('$count', style: TextStyle(fontSize: 48)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => store.dispatch(const Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

Done. Predictable state with automatic UI updates.


Why Watchable Redux? #

Feature Watchable Redux flutter_redux redux.dart
Selector Caching O(1) built-in Manual Manual
Boilerplate Minimal Moderate Heavy
Memoized Selectors Built-in External pkg External pkg
Async Actions Thunk + Effects Thunk only Thunk only
Time-Travel Built-in DevTools External External
Type Safety Full generics Good Good
Learning Curve 10 minutes 30 minutes 1 hour

Core Concepts #

Actions #

// Simple action
class Increment extends ReduxAction {
  const Increment();
}

// Action with payload
class SetValue extends ReduxAction {
  final int value;
  const SetValue(this.value);
}

// Sealed class for exhaustive matching
sealed class CounterAction extends ReduxAction {
  const CounterAction();
}

class Add extends CounterAction {
  final int amount;
  const Add(this.amount);
}

class Reset extends CounterAction {
  const Reset();
}

Reducers #

// Pattern matching reducer
int counterReducer(int state, ReduxAction action) {
  return switch (action) {
    Add(:final amount) => state + amount,
    Reset() => 0,
    _ => state,
  };
}

// Complex state reducer
AppState appReducer(AppState state, ReduxAction action) {
  return switch (action) {
    LoginSuccess(:final user, :final token) => state.copyWith(
      user: user,
      token: token,
      isLoading: false,
    ),
    Logout() => AppState.initial(),
    _ => state,
  };
}

Store #

final store = Store<AppState>(
  initialState: AppState.initial(),
  reducer: appReducer,
  middlewares: [thunkMiddleware(), loggerMiddleware()],
  enableDevTools: true,  // Time-travel debugging
);

// Access state
print(store.state);

// Dispatch actions
store.dispatch(const Increment());

// Select with caching
final counter = store.select((s) => s.counter);

Selectors #

Simple Selectors #

// Direct field access
final selectUser = (AppState s) => s.user;
final selectCart = (AppState s) => s.cart.items;

// Use in UI
store.select(selectUser).build((user) => Text(user.name));

Memoized Selectors #

// Cached computation - only recalculates when input changes
final selectTotal = createSelector<CartState, List<CartItem>, double>(
  (s) => s.items,
  (items) => items.fold(0.0, (sum, item) => sum + item.price * item.quantity),
);

// Compose multiple inputs
final selectFinalPrice = createSelector2<CartState, double, double, double>(
  (s) => s.subtotal,
  (s) => s.discount,
  (subtotal, discount) => subtotal * (1 - discount),
);

// Chain selectors (up to 5 inputs)
final selectSummary = createSelector3<AppState, User?, List<CartItem>, double, String>(
  (s) => s.user,
  (s) => s.cart.items,
  (s) => s.cart.total,
  (user, items, total) => '${user?.name}: ${items.length} items, \$$total',
);

O(1) Selector Caching #

// Same selector = same cached result
final doubled1 = store.select((s) => s * 2);
final doubled2 = store.select((s) => s * 2);

print(identical(doubled1, doubled2)); // true - same cached instance

Middleware #

Thunk (Async Actions) #

final store = Store<AppState>(
  initialState: AppState.initial(),
  reducer: appReducer,
  middlewares: [thunkMiddleware()],
);

// Dispatch async action
store.dispatch(ThunkAction<AppState>((dispatch, getState) async {
  dispatch(SetLoading(true));

  try {
    final user = await api.fetchUser();
    dispatch(SetUser(user));
  } catch (e) {
    dispatch(SetError(e.toString()));
  } finally {
    dispatch(SetLoading(false));
  }
}));

Effects (Stream-based Side Effects) #

final effects = EffectsMiddleware<AppState>(effects: [
  // Auto-save on data change
  (action, store) {
    if (action is DataChanged) {
      return Stream.fromFuture(saveToStorage(store.state))
        .map((_) => const SaveSuccess());
    }
    return null;
  },

  // Debounced search
  (action, store) {
    if (action is SearchQueryChanged) {
      return Stream.fromFuture(
        Future.delayed(Duration(milliseconds: 300))
          .then((_) => api.search(action.query))
      ).map((results) => SearchResults(results));
    }
    return null;
  },
]);

final store = Store<AppState>(
  initialState: AppState.initial(),
  reducer: appReducer,
  middlewares: [effects.middleware],
);

// Don't forget to dispose
effects.dispose();

Logger #

final store = Store<int>(
  initialState: 0,
  reducer: counterReducer,
  middlewares: [
    loggerMiddleware(
      logActions: true,
      logState: true,
    ),
  ],
);

Custom Middleware #

Middleware<AppState> authMiddleware() {
  return (store, action, next) {
    // Block actions if not authenticated
    if (action is ProtectedAction && store.state.token == null) {
      store.dispatch(const RedirectToLogin());
      return; // Block action
    }
    next(action); // Continue chain
  };
}

DevTools (Time-Travel) #

final store = Store<int>(
  initialState: 0,
  reducer: counterReducer,
  enableDevTools: true,
  maxHistoryLength: 100,
);

// Navigate history
store.devTools?.undo();
store.devTools?.redo();
store.devTools?.jumpTo(5);
store.devTools?.reset();

// Query state
print(store.devTools?.canUndo);  // true/false
print(store.devTools?.canRedo);  // true/false
print(store.devTools?.history);  // List<StateSnapshot>
print(store.devTools?.currentIndex); // Current position

// Build DevTools UI
store.devTools!.watchableHistory.build((history) =>
  ListView.builder(
    itemCount: history.length,
    itemBuilder: (_, i) => ListTile(
      title: Text('State $i'),
      subtitle: Text('${history[i].state}'),
      onTap: () => store.devTools!.jumpTo(i),
    ),
  ),
);

Provider #

StoreProvider #

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(home: HomePage()),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final store = StoreProvider.of<AppState>(context);
    // or
    final store = context.store<AppState>();

    return store.select((s) => s.user).build(
      (user) => Text('Hello, ${user.name}'),
    );
  }
}

Global Redux Singleton #

void main() {
  Redux.init(store);
  runApp(MyApp());
}

// Access anywhere
Redux.dispatch(const Increment());
print(Redux.state<AppState>());

Real-World Examples #

Authentication Flow #

// State
class AuthState {
  final User? user;
  final String? token;
  final bool isLoading;
  final String? error;

  const AuthState({
    this.user,
    this.token,
    this.isLoading = false,
    this.error,
  });
}

// Actions
sealed class AuthAction extends ReduxAction {}
class LoginRequest extends AuthAction {
  final String email, password;
  LoginRequest(this.email, this.password);
}
class LoginSuccess extends AuthAction {
  final User user;
  final String token;
  LoginSuccess(this.user, this.token);
}
class LoginFailure extends AuthAction {
  final String error;
  LoginFailure(this.error);
}
class Logout extends AuthAction {}

// Reducer
AuthState authReducer(AuthState state, ReduxAction action) {
  return switch (action) {
    LoginRequest() => state.copyWith(isLoading: true, error: null),
    LoginSuccess(:final user, :final token) => AuthState(user: user, token: token),
    LoginFailure(:final error) => state.copyWith(isLoading: false, error: error),
    Logout() => const AuthState(),
    _ => state,
  };
}

// Thunk
void login(String email, String password) {
  store.dispatch(ThunkAction<AuthState>((dispatch, getState) async {
    dispatch(LoginRequest(email, password));
    try {
      final result = await authApi.login(email, password);
      dispatch(LoginSuccess(result.user, result.token));
    } catch (e) {
      dispatch(LoginFailure(e.toString()));
    }
  }));
}

// UI
store.select((s) => s.isLoading).build((loading) =>
  loading ? CircularProgressIndicator() : LoginForm()
);

Shopping Cart #

// State
class CartState {
  final List<CartItem> items;
  final String? coupon;
  final double discount;

  double get subtotal => items.fold(0, (s, i) => s + i.price * i.quantity);
  double get total => subtotal * (1 - discount);
}

// Selectors
final selectItemCount = createSelector<CartState, List<CartItem>, int>(
  (s) => s.items,
  (items) => items.fold(0, (sum, i) => sum + i.quantity),
);

final selectTotalPrice = createSelector2<CartState, double, double, double>(
  (s) => s.subtotal,
  (s) => s.discount,
  (subtotal, discount) => subtotal * (1 - discount),
);

// UI
Column(children: [
  store.select(selectItemCount).build((count) =>
    Text('$count items in cart')
  ),
  store.select(selectTotalPrice).build((total) =>
    Text('Total: \$${total.toStringAsFixed(2)}')
  ),
]);

Architecture Patterns #

Feature-Based Structure #

lib/
  features/
    auth/
      auth_actions.dart
      auth_reducer.dart
      auth_selectors.dart
      auth_middleware.dart
    cart/
      cart_actions.dart
      cart_reducer.dart
      cart_selectors.dart
  store/
    app_state.dart
    app_reducer.dart
    store.dart

Combine Reducers #

Map<String, dynamic> appReducer(Map<String, dynamic> state, ReduxAction action) {
  return combineReducers({
    'auth': (s, a) => authReducer(s ?? AuthState(), a),
    'cart': (s, a) => cartReducer(s ?? CartState(), a),
    'settings': (s, a) => settingsReducer(s ?? SettingsState(), a),
  })(state, action);
}

Performance #

State Management Comparison #

Comprehensive benchmarks comparing watchable_redux with other popular Flutter state management solutions.

Throughput (Operations/Second)

Listeners ValueNotifier Provider Riverpod watchable_redux
0 24.9M 23.5M 20.1M 18.5M
10 14.3M 14.3M 6.1M 19.1M
100 3.0M 3.0M 912K 18.9M

Key Finding: watchable_redux is the fastest with multiple listeners! With 100 listeners, it's 6x faster than ValueNotifier due to O(1) selector caching and optimized Watchable internals using identical() comparison.

Widget Rebuild Efficiency

After updating only auth state in a widget tree with 3 widgets (auth, theme, posts):

Library Auth Theme Posts Total Rebuilds
ValueNotifier 1 1 1 3
Provider 1 0 0 1
Riverpod 1 0 0 1
watchable_redux 1 0 0 1

Key Finding: ValueNotifier rebuilds ALL widgets on ANY state change. Provider/Riverpod/watchable_redux rebuild only affected widgets.

Feature Comparison

Feature ValueNotifier Provider Riverpod watchable_redux
Raw Update Speed ★★★★☆ ★★★★☆ ★★★☆☆ ★★★★★
Selective Rebuilds ☆☆☆☆☆ ★★★★☆ ★★★★★ ★★★★★
Boilerplate ★★★★★ ★★★★☆ ★★★☆☆ ★★★★☆
Type Safety ★★★★★ ★★★★☆ ★★★★★ ★★★★★
DevTools/Time-Travel ☆☆☆☆☆ ☆☆☆☆☆ ★★★☆☆ ★★★★★
Async Support ☆☆☆☆☆ ★★☆☆☆ ★★★★☆ ★★★★★
Testing ★★★★★ ★★★★☆ ★★★★★ ★★★★★
Learning Curve ★★★★★ ★★★★☆ ★★★☆☆ ★★★★☆
Middleware/Effects ☆☆☆☆☆ ☆☆☆☆☆ ★★★☆☆ ★★★★★

When to Use Each

Solution Best For
ValueNotifier Simple counters, form fields, local widget state
Provider Medium apps, gradual adoption, InheritedWidget replace
Riverpod Complex apps, heavy testing needs, compile-time safety
watchable_redux Enterprise apps, debugging needs, Redux familiarity

Internal Benchmarks #

All operations are optimized for O(1) time complexity where possible.

Operation Performance Notes
Dispatch (simple state) ~1μs Single int state
Dispatch (complex state) <1μs Nested objects
Dispatch (10k list) ~41μs Immutable append
Selector lookup O(1) Constant regardless of cache size
Selector chain (20 deep) <1μs Memoization eliminates overhead
Undo/Redo O(1) Instant time-travel
JumpTo O(1) Direct index access

Stress Test Results #

Test Result
100,000 dispatches 16-23ms
50,000 mixed actions 5-7ms
100 dispatches (1MB state) 55ms
10k selector chain calls (25 deep) 2ms
10k undo/redo pairs 7ms
10k jumpTo operations 1ms
100 stores × 1000 dispatches 12ms

DevTools Overhead #

  • Without DevTools: ~18.9M ops/sec
  • With DevTools (100 history): ~1.5M ops/sec
  • Overhead: ~92% (expected due to history recording)

Note: DevTools should only be enabled during development.

Performance Tips #

  1. Use memoized selectors for computed values
  2. Selector caching is automatic - same selector = same cached result
  3. Granular selects - select only what you need
  4. Avoid inline selectors in hot paths - define them once
  5. Disable DevTools in production for maximum throughput
// Good - defined once, cached
final selectUserName = (AppState s) => s.user?.name;
store.select(selectUserName).build((name) => Text(name ?? 'Guest'));

// Avoid in hot rebuild paths - creates new selector each time
store.select((s) => s.user?.name).build((name) => Text(name ?? 'Guest'));

Run Benchmarks #

# Run state management comparison
flutter test test/performance/state_management_comparison_test.dart

# Run all performance tests
flutter test test/performance/

Testing #

Test Coverage #

The package includes 327+ tests covering:

Category Tests Coverage
Store ~25 Creation, dispatch, dispose
Selectors ~30 Memoization, caching, chaining
Middleware ~25 Thunk, effects, custom
DevTools ~35 Time-travel, history
Reducers ~40 All types, combineReducers
Provider ~20 Widget tree, context
Integration ~25 Full workflows
Performance ~60 O(1) verification, benchmarks
Edge Cases ~32 Error handling, concurrency
Thread Safety ~15 Race conditions, torn reads

Run tests:

flutter test

Writing Tests #

test('reducer handles increment', () {
  expect(counterReducer(0, const Increment()), equals(1));
});

test('selector memoization', () {
  var computeCount = 0;
  final selector = createSelector<int, int, int>(
    (s) => s,
    (s) { computeCount++; return s * 2; },
  );

  selector(5);
  selector(5);
  expect(computeCount, equals(1)); // Computed only once
});

testWidgets('store updates UI', (tester) async {
  final store = Store<int>(initialState: 0, reducer: counterReducer);

  await tester.pumpWidget(
    MaterialApp(
      home: store.select((s) => s).build((count) => Text('$count')),
    ),
  );

  expect(find.text('0'), findsOneWidget);

  store.dispatch(const Increment());
  await tester.pump();

  expect(find.text('1'), findsOneWidget);
});

Migration from flutter_redux #

// flutter_redux
StoreProvider<AppState>(
  store: store,
  child: StoreConnector<AppState, int>(
    converter: (store) => store.state.counter,
    builder: (context, count) => Text('$count'),
  ),
)

// watchable_redux
StoreProvider<AppState>(
  store: store,
  child: Builder(
    builder: (context) => context.store<AppState>()
      .select((s) => s.counter)
      .build((count) => Text('$count')),
  ),
)

API Reference #

Store #

  • Store<S>({initialState, reducer, middlewares, enableDevTools, maxHistoryLength})
  • state - Current state
  • dispatch(action) - Dispatch action
  • select<R>(selector) - Get cached watchable for selector
  • devTools - Access time-travel DevTools
  • dispose() - Clean up resources

Selectors #

  • createSelector<S, I, R>(inputSelector, compute) - 1 input
  • createSelector2 through createSelector5 - 2-5 inputs

Middleware #

  • thunkMiddleware<S>() - Async action support
  • loggerMiddleware({logActions, logState}) - Debug logging
  • EffectsMiddleware<S>({effects}) - Stream-based side effects

DevTools #

  • undo(), redo(), jumpTo(index), reset(), clear()
  • canUndo, canRedo, history, currentIndex
  • watchableHistory, watchableIndex - Reactive history

Provider #

  • StoreProvider<S>({store, child})
  • StoreProvider.of<S>(context), StoreProvider.maybeOf<S>(context)
  • context.store<S>(), context.dispatch<S>(action)
  • Redux.init(store), Redux.dispatch(action), Redux.state<S>()

License #

BSD-3-Clause. See LICENSE.


Built with Watchable - The simplest Flutter state management.

0
likes
0
points
11
downloads

Documentation

Documentation

Publisher

verified publisherdipendrasharma.com

Weekly Downloads

Predictable state management for Flutter. Redux architecture with O(1) selector caching, memoized selectors, async middleware, and time-travel debugging. Built on Watchable.

Repository (GitHub)
View/report issues

Topics

#state-management #redux #flutter #reactive #watchable

License

unknown (license)

Dependencies

flutter, watchable

More

Packages that depend on watchable_redux