名称: asyncredux-state-design
描述: 遵循 AsyncRedux 最佳实践设计不可变状态类。包括创建具有 copy() 方法的 AppState 类,定义 initialState(),组合嵌套状态对象,以及可选用 fast_immutable_collections 包处理 IList、ISet 和 IMap。
AsyncRedux 状态设计
核心原则:不可变性
状态类必须是不可变的——字段在创建后不能被修改。不是直接改变状态,而是创建新实例。所有字段都应标记为 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,
);
}
关键组件
- Final 字段 - 所有状态字段必须为
final initialState()方法 - 静态工厂方法提供默认值copy()方法 - 创建修改后的实例而不改变原始对象
copy() 方法模式
copy() 方法接受每个字段的可选参数。如果参数为 null,则保留现有值:
AppState copy({String? name, int? age}) =>
AppState(
name: name ?? this.name,
age: age ?? this.age,
);
还可以添加便捷方法:
AppState withName(String name) => copy(name: name);
AppState withAge(int age) => copy(age: age);
嵌套/复合状态
对于复杂应用,在单个 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,
);
}
每个嵌套类遵循相同模式:
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,
);
}
在 Actions 中更新嵌套状态
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);
}
}
使用 fast_immutable_collections
对于列表、集合和映射,使用 fast_immutable_collections 包(由 AsyncRedux 作者开发):
dependencies:
fast_immutable_collections: ^10.0.0
IList 示例
在构造函数和 copy 方法中使用 Iterable,通过 IList.orNull() 进行转换。这允许调用者传递任何可迭代对象(List、Set、IList),无需手动转换:
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);
// 带有业务逻辑的便捷方法
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)),
);
}
// 灵活用法:
var state = AppState(); // 空列表
var state = AppState(todos: [todo1, todo2]); // List 有效
var state = AppState(todos: {todo1, todo2}); // Set 有效
var state = AppState(todos: existingIList); // IList 重用(不复制)
IMap 示例
在构造函数和 copy 方法中使用 Map,通过 IMap.orNull() 进行转换:
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));
}
ISet 示例
在构造函数和 copy 方法中使用 Iterable,通过 ISet.orNull() 进行转换:
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),
);
}
状态中的事件
对于一次性 UI 交互(滚动、文本字段变化),使用 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,
);
}
事件初始化为“已消耗”,在 Actions 中替换为新实例时变为活跃。
状态类中的业务逻辑
AsyncRedux 建议将业务逻辑放在状态类中,而不是 Actions 或 Widgets:
class TodoList {
final IList<Todo> items;
TodoList({required this.items});
// 业务逻辑方法
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 变为简单的协调器:
class AddTodo extends ReduxAction<AppState> {
final Todo todo;
AddTodo(this.todo);
@override
AppState reduce() => state.copy(
todoList: state.todoList.addTodo(todo),
);
}
Actions 中的状态访问
Actions 通过 getters 访问状态:
state- 当前状态(在异步 Actions 的每个await后更新)initialState- 动作首次分发时的状态(永不改变)
class MyAction extends ReduxAction<AppState> {
@override
Future<AppState?> reduce() async {
var originalValue = initialState.counter; // 保留原始值
await someAsyncWork();
var currentValue = state.counter; // 可能已改变
return state.copy(counter: currentValue + 1);
}
}
测试优势
不可变状态和纯方法使得单元测试变得直接:
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); // 原始状态未改变
});
}
参考资料
文档中的 URL:
- https://asyncredux.com/flutter/basics/state
- https://asyncredux.com/flutter/basics/sync-actions
- https://asyncredux.com/flutter/basics/changing-state-is-optional
- https://asyncredux.com/flutter/basics/actions-and-reducers
- https://asyncredux.com/flutter/basics/async-actions
- https://asyncredux.com/flutter/basics/events
- https://asyncredux.com/flutter/advanced-actions/redux-action
- https://asyncredux.com/flutter/miscellaneous/business-logic
- https://asyncredux.com/flutter/miscellaneous/persistence
- https://asyncredux.com/flutter/connector/store-connector
- https://asyncredux.com/flutter/testing/mocking
- https://asyncredux.com/flutter/intro
- https://asyncredux.com/flutter/about