watchable_redux 1.0.0
watchable_redux: ^1.0.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/main.dart
import 'package:flutter/material.dart';
import 'package:watchable_redux/watchable_redux.dart';
// State
class AppState {
final int counter;
final List<String> todos;
final bool isLoading;
const AppState({
this.counter = 0,
this.todos = const [],
this.isLoading = false,
});
AppState copyWith({
int? counter,
List<String>? todos,
bool? isLoading,
}) {
return AppState(
counter: counter ?? this.counter,
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
);
}
}
// Actions
sealed class AppAction extends ReduxAction {
const AppAction();
}
class Increment extends AppAction {
const Increment();
}
class Decrement extends AppAction {
const Decrement();
}
class AddTodo extends AppAction {
final String text;
const AddTodo(this.text);
}
class RemoveTodo extends AppAction {
final int index;
const RemoveTodo(this.index);
}
class SetLoading extends AppAction {
final bool loading;
const SetLoading(this.loading);
}
// Reducer
AppState appReducer(AppState state, ReduxAction action) {
return switch (action) {
Increment() => state.copyWith(counter: state.counter + 1),
Decrement() => state.copyWith(counter: state.counter - 1),
AddTodo(:final text) => state.copyWith(todos: [...state.todos, text]),
RemoveTodo(:final index) => state.copyWith(
todos: [...state.todos]..removeAt(index),
),
SetLoading(:final loading) => state.copyWith(isLoading: loading),
_ => state,
};
}
// Selectors
final selectCounter = (AppState s) => s.counter;
final selectTodos = (AppState s) => s.todos;
final selectTodoCount = createSelector<AppState, List<String>, int>(
selectTodos,
(todos) => todos.length,
);
// Async action example
ThunkAction<AppState> fetchTodosAsync() {
return ThunkAction((dispatch, getState) async {
dispatch(const SetLoading(true));
await Future.delayed(const Duration(seconds: 1));
dispatch(const AddTodo('Fetched todo 1'));
dispatch(const AddTodo('Fetched todo 2'));
dispatch(const SetLoading(false));
});
}
// Store
final store = Store<AppState>(
initialState: const AppState(),
reducer: appReducer,
middlewares: [
loggerMiddleware(),
thunkMiddleware(),
],
enableDevTools: true,
);
void main() {
Redux.init(store);
runApp(const MyApp());
}
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(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
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'),
actions: [
IconButton(
icon: const Icon(Icons.undo),
onPressed: () => store.devTools?.undo(),
),
IconButton(
icon: const Icon(Icons.redo),
onPressed: () => store.devTools?.redo(),
),
],
),
body: const Column(
children: [
CounterSection(),
Divider(),
TodoSection(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => store.dispatch(fetchTodosAsync()),
child: const Icon(Icons.download),
),
);
}
}
class CounterSection extends StatelessWidget {
const CounterSection({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => Redux.dispatch(const Decrement()),
),
store.build(
selectCounter,
(count) => Text(
'$count',
style: Theme.of(context).textTheme.headlineMedium,
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Redux.dispatch(const Increment()),
),
],
),
);
}
}
class TodoSection extends StatelessWidget {
const TodoSection({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
'Todos: ',
style: Theme.of(context).textTheme.titleMedium,
),
store.build(
selectTodoCount,
(count) => Text(
'$count',
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),
Expanded(
child: store.build(
selectTodos,
(todos) => ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(
title: Text(todos[index]),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => Redux.dispatch(RemoveTodo(index)),
),
),
),
),
),
],
),
);
}
}