xiaohongshu
xiaohongshu
asyncredux-state-design
Design immutable state classes following AsyncRedux best practices. Includes creating the AppState class with a `copy()` method, defining `initialState()`, composing nested state objects, and optionally using the fast_immutable_collections package for IList, ISet, and IMap.
Full skill instructions
State classes must be immutable—fields cannot be modified after creation. Instead of changing state directly, you create new instances. All fields should be marked final.
class AppState {
final String name;
final int age;
AppState({required this.name, required this.age});
static AppState initialState() => AppState(name: "", age: 0);
AppState copy({String? name, int? age}) =>
AppState(
name: name ?? this.name,
age: age ?? this.age,
);
}
finalinitialState() - Static factory method providing default valuescopy() method - Creates modified instances without mutating originalThe copy() method accepts optional parameters for each field. If a parameter is null, it keeps the existing value:
AppState copy({String? name, int? age}) =>
AppState(
name: name ?? this.name,
age: age ?? this.age,
);
You can also add convenience methods:
AppState withName(String name) => copy(name: name);
AppState withAge(int age) => copy(age: age);
For complex applications, compose multiple state classes within a single AppState:
class AppState {
final TodoList todoList;
final User user;
final Settings settings;
AppState({
required this.todoList,
required this.user,
required this.settings,
});
static AppState initialState() => AppState(
todoList: TodoList.initialState(),
user: User.initialState(),
settings: Settings.initialState(),
);
AppState copy({
TodoList? todoList,
User? user,
Settings? settings,
}) =>
AppState(
todoList: todoList ?? this.todoList,
user: user ?? this.user,
settings: settings ?? this.settings,
);
}
Each nested class follows the same pattern:
class User {
final String name;
final String email;
User({required this.name, required this.email});
static User initialState() => User(name: "", email: "");
User copy({String? name, String? email}) =>
User(
name: name ?? this.name,
email: email ?? this.email,
);
}
class UpdateUserName extends ReduxAction<AppState> {
final String name;
UpdateUserName(this.name);
@override
AppState reduce() {
var newUser = state.user.copy(name: name);
return state.copy(user: newUser);
}
}
For lists, sets, and maps, use the fast_immutable_collections package (by the same author as AsyncRedux):
dependencies:
fast_immutable_collections: ^10.0.0
Use Iterable in constructors and copy methods, with IList.orNull() for conversion. This lets callers pass any iterable (List, Set, IList) without manual conversion:
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
class AppState {
final IList<Todo> todos;
AppState({
Iterable<Todo>? todos,
}) : todos = IList.orNull(todos) ?? const IList.empty();
static AppState initialState() => AppState();
AppState copy({Iterable<Todo>? todos}) =>
AppState(todos: IList.orNull(todos) ?? this.todos);
// Convenience methods with business logic
AppState addTodo(Todo todo) => copy(todos: todos.add(todo));
AppState removeTodo(Todo todo) => copy(todos: todos.remove(todo));
AppState toggleTodo(int index) => copy(
todos: todos.replace(index, todos[index].copy(done: !todos[index].done)),
);
}
// Flexible usage:
var state = AppState(); // Empty list
var state = AppState(todos: [todo1, todo2]); // List works
var state = AppState(todos: {todo1, todo2}); // Set works
var state = AppState(todos: existingIList); // IList reused (no copy)
Use Map in constructors and copy methods, with IMap.orNull() for conversion:
class AppState {
final IMap<String, User> usersById;
AppState({
Map<String, User>? usersById,
}) : usersById = IMap.orNull(usersById) ?? const IMap.empty();
static AppState initialState() => AppState();
AppState copy({Map<String, User>? usersById}) =>
AppState(usersById: IMap.orNull(usersById) ?? this.usersById);
AppState addUser(User user) => copy(usersById: usersById.add(user.id, user));
AppState removeUser(String id) => copy(usersById: usersById.remove(id));
}
Use Iterable in constructors and copy methods, with ISet.orNull() for conversion:
class AppState {
final ISet<String> selectedIds;
AppState({
Iterable<String>? selectedIds,
}) : selectedIds = ISet.orNull(selectedIds) ?? const ISet.empty();
static AppState initialState() => AppState();
AppState copy({Iterable<String>? selectedIds}) =>
AppState(selectedIds: ISet.orNull(selectedIds) ?? this.selectedIds);
AppState toggleSelection(String id) => copy(
selectedIds: selectedIds.contains(id)
? selectedIds.remove(id)
: selectedIds.add(id),
);
}
For one-time UI interactions (scrolling, text field changes), use Evt:
class AppState {
final Evt clearTextEvt;
final Evt<String> changeTextEvt;
AppState({
required this.clearTextEvt,
required this.changeTextEvt,
});
static AppState initialState() => AppState(
clearTextEvt: Evt.spent(),
changeTextEvt: Evt<String>.spent(),
);
AppState copy({
Evt? clearTextEvt,
Evt<String>? changeTextEvt,
}) =>
AppState(
clearTextEvt: clearTextEvt ?? this.clearTextEvt,
changeTextEvt: changeTextEvt ?? this.changeTextEvt,
);
}
Events are initialized as "spent" and become active when replaced with new instances in actions.
AsyncRedux recommends placing business logic in state classes, not in actions or widgets:
class TodoList {
final IList<Todo> items;
TodoList({required this.items});
// Business logic methods
int get completedCount => items.where((t) => t.done).length;
int get pendingCount => items.length - completedCount;
double get completionRate => items.isEmpty ? 0 : completedCount / items.length;
IList<Todo> get completed => items.where((t) => t.done).toIList();
IList<Todo> get pending => items.where((t) => !t.done).toIList();
TodoList addTodo(Todo todo) => TodoList(items: items.add(todo));
TodoList removeTodo(Todo todo) => TodoList(items: items.remove(todo));
}
Actions become simple orchestrators:
class AddTodo extends ReduxAction<AppState> {
final Todo todo;
AddTodo(this.todo);
@override
AppState reduce() => state.copy(
todoList: state.todoList.addTodo(todo),
);
}
Actions access state through getters:
state - Current state (updates after each await in async actions)initialState - State when the action was first dispatched (never changes)class MyAction extends ReduxAction<AppState> {
@override
Future<AppState?> reduce() async {
var originalValue = initialState.counter; // Preserved
await someAsyncWork();
var currentValue = state.counter; // May have changed
return state.copy(counter: currentValue + 1);
}
}
Immutable state with pure methods makes unit testing straightforward:
void main() {
test('addTodo adds item to list', () {
var state = AppState.initialState();
var todo = Todo(text: 'Test', done: false);
var newState = state.addTodo(todo);
expect(newState.todos.length, 1);
expect(newState.todos.first.text, 'Test');
expect(state.todos.length, 0); // Original unchanged
});
}
URLs from the documentation:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience