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
199
downloads

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

Documentation

Documentation
API reference

License

BSD-3-Clause (license)

Dependencies

flutter, watchable

More

Packages that depend on watchable_redux