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

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

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:watchable_redux/watchable_redux.dart';

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

// =============================================================================
// STATE
// =============================================================================
class AppState {
  final int counter;
  final String name;
  final bool isLoading;
  final List<String> items;

  const AppState({
    this.counter = 0,
    this.name = 'Guest',
    this.isLoading = false,
    this.items = const [],
  });

  AppState copyWith(
      {int? counter, String? name, bool? isLoading, List<String>? items}) {
    return AppState(
      counter: counter ?? this.counter,
      name: name ?? this.name,
      isLoading: isLoading ?? this.isLoading,
      items: items ?? this.items,
    );
  }
}

// =============================================================================
// ACTIONS
// =============================================================================
class Increment extends ReduxAction {
  const Increment();
}

class Decrement extends ReduxAction {
  const Decrement();
}

class SetName extends ReduxAction {
  final String name;
  const SetName(this.name);
}

class SetLoading extends ReduxAction {
  final bool loading;
  const SetLoading(this.loading);
}

class AddItem extends ReduxAction {
  final String item;
  const AddItem(this.item);
}

class ClearItems extends ReduxAction {
  const ClearItems();
}

// =============================================================================
// REDUCER
// =============================================================================
AppState appReducer(AppState state, ReduxAction action) {
  return switch (action) {
    Increment() => state.copyWith(counter: state.counter + 1),
    Decrement() => state.copyWith(counter: state.counter - 1),
    SetName(:final name) => state.copyWith(name: name),
    SetLoading(:final loading) => state.copyWith(isLoading: loading),
    AddItem(:final item) => state.copyWith(items: [...state.items, item]),
    ClearItems() => state.copyWith(items: []),
    _ => state,
  };
}

// =============================================================================
// SELECTORS (Memoized)
// =============================================================================
int selectCounter(AppState s) => s.counter;
String selectName(AppState s) => s.name;
List<String> selectItems(AppState s) => s.items;
bool selectIsLoading(AppState s) => s.isLoading;

final selectItemCount = createSelector<AppState, List<String>, int>(
  selectItems,
  (items) => items.length,
);

final selectGreeting = createSelector2<AppState, String, int, String>(
  selectName,
  selectCounter,
  (name, count) => '$name clicked $count times',
);

// =============================================================================
// STORE
// =============================================================================
final store = Store<AppState>(
  initialState: const AppState(),
  reducer: appReducer,
  middlewares: [thunkMiddleware(), loggerMiddleware()],
  enableDevTools: true,
);

// =============================================================================
// APP
// =============================================================================
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        title: 'Watchable Redux Demo',
        theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
        home: const HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Watchable Redux Demo')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCounterSection(),
          _buildNameSection(),
          _buildSelectorsSection(),
          _buildItemsSection(),
          _buildThunkSection(),
          _buildDevToolsSection(),
        ],
      ),
    );
  }

  Widget _section(String title, List<Widget> children) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title,
                style:
                    const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 12),
            ...children,
          ],
        ),
      ),
    );
  }

  // Counter with dispatch
  Widget _buildCounterSection() {
    return _section('Counter (dispatch)', [
      store.build(
        (count) => Text('Count: $count', style: const TextStyle(fontSize: 24)),
        only: selectCounter,
      ),
      const SizedBox(height: 8),
      Row(children: [
        ElevatedButton(
          onPressed: () => store.dispatch(const Increment()),
          child: const Text('+'),
        ),
        const SizedBox(width: 8),
        ElevatedButton(
          onPressed: () => store.dispatch(const Decrement()),
          child: const Text('-'),
        ),
      ]),
    ]);
  }

  // Name input
  Widget _buildNameSection() {
    return _section('Name Input', [
      store.build(
        (name) => Text('Hello, $name!', style: const TextStyle(fontSize: 20)),
        only: selectName,
      ),
      const SizedBox(height: 8),
      TextField(
        onChanged: (v) => store.dispatch(SetName(v.isEmpty ? 'Guest' : v)),
        decoration: const InputDecoration(
            labelText: 'Enter name', border: OutlineInputBorder()),
      ),
    ]);
  }

  // Memoized selectors
  Widget _buildSelectorsSection() {
    return _section('Memoized Selectors', [
      const Text('createSelector2:',
          style: TextStyle(fontWeight: FontWeight.bold)),
      store.build(
        (greeting) =>
            Text(greeting, style: const TextStyle(color: Colors.blue)),
        only: selectGreeting,
      ),
      const SizedBox(height: 8),
      const Text('createSelector (derived):',
          style: TextStyle(fontWeight: FontWeight.bold)),
      store.build(
        (count) => Text('Item count: $count',
            style: const TextStyle(color: Colors.green)),
        only: selectItemCount,
      ),
    ]);
  }

  // Items list
  Widget _buildItemsSection() {
    return _section('Items List', [
      store.build(
        (items) => Text(items.isEmpty ? 'No items' : items.join(', ')),
        only: selectItems,
      ),
      const SizedBox(height: 8),
      Row(children: [
        ElevatedButton(
          onPressed: () =>
              store.dispatch(AddItem('Item ${DateTime.now().second}')),
          child: const Text('Add Item'),
        ),
        const SizedBox(width: 8),
        ElevatedButton(
          onPressed: () => store.dispatch(const ClearItems()),
          child: const Text('Clear'),
        ),
      ]),
    ]);
  }

  // Thunk (async action)
  Widget _buildThunkSection() {
    return _section('Thunk (Async)', [
      store.build(
        (loading) => loading
            ? const Row(children: [
                CircularProgressIndicator(),
                SizedBox(width: 8),
                Text('Loading...')
              ])
            : const Text('Ready', style: TextStyle(color: Colors.green)),
        only: selectIsLoading,
      ),
      const SizedBox(height: 8),
      ElevatedButton(
        onPressed: () =>
            store.dispatch(ThunkAction<AppState>((dispatch, getState) async {
          dispatch(const SetLoading(true));
          await Future.delayed(const Duration(seconds: 1));
          dispatch(SetName('Loaded User'));
          dispatch(const SetLoading(false));
        })),
        child: const Text('Load User (Thunk)'),
      ),
    ]);
  }

  // DevTools
  Widget _buildDevToolsSection() {
    return _section('DevTools (Time-Travel)', [
      ValueListenableBuilder(
        valueListenable: store.devTools!.watchableHistory.notifier,
        builder: (_, history, __) => Text('History: ${history.length} states'),
      ),
      const SizedBox(height: 8),
      Row(children: [
        ElevatedButton(
          onPressed: store.devTools?.canUndo == true
              ? () => store.devTools?.undo()
              : null,
          child: const Text('Undo'),
        ),
        const SizedBox(width: 8),
        ElevatedButton(
          onPressed: store.devTools?.canRedo == true
              ? () => store.devTools?.redo()
              : null,
          child: const Text('Redo'),
        ),
        const SizedBox(width: 8),
        ElevatedButton(
          onPressed: () => store.devTools?.reset(),
          child: const Text('Reset'),
        ),
      ]),
    ]);
  }
}
0
likes
160
points
11
downloads

Documentation

Documentation
API reference

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

BSD-3-Clause (license)

Dependencies

flutter, watchable

More

Packages that depend on watchable_redux