名称: asyncredux-optimistic-update-mixin 描述: 添加OptimisticUpdate混合器,用于在服务器确认前提供即时UI反馈。涵盖即时状态更改、失败时自动回滚,以及可选地通知用户回滚。
乐观更新混合器
AsyncRedux为不同场景提供了三种乐观更新混合器:
| 混合器 | 使用场景 |
|---|---|
OptimisticCommand |
一次性操作(创建、删除、提交)带回滚 |
OptimisticSync |
快速切换/交互,带合并 |
OptimisticSyncWithPush |
实时服务器推送场景,带版本跟踪 |
OptimisticCommand
用于一次性服务器操作,其中即时UI反馈重要:创建待办事项、删除项目、提交表单或处理支付。
基本示例
没有乐观更新(用户等待服务器):
class SaveTodo extends AppAction {
final Todo newTodo;
SaveTodo(this.newTodo);
Future<AppState?> reduce() async {
await saveTodo(newTodo);
var reloadedList = await loadTodoList();
return state.copy(todoList: reloadedList);
}
}
使用OptimisticCommand(即时UI反馈):
class SaveTodo extends AppAction with OptimisticCommand {
final Todo newTodo;
SaveTodo(this.newTodo);
// 立即应用于UI的值
Object? optimisticValue() => newTodo;
// 从状态提取当前值(用于回滚比较)
Object? getValueFromState(AppState state)
=> state.todoList.getById(newTodo.id);
// 应用值到状态并返回新状态
AppState applyValueToState(AppState state, Object? value)
=> state.copy(todoList: state.todoList.add(value as Todo));
// 发送到服务器(如果使用Retry混合器,则重试)
Future<Object?> sendCommandToServer(Object? value) async
=> await saveTodo(newTodo);
// 可选:错误时从服务器重新加载
Future<Object?> reloadFromServer() async
=> await loadTodoList();
}
回滚如何工作
如果sendCommandToServer失败,混合器仅当当前状态仍匹配乐观值时才自动回滚。这避免了在请求进行中撤销更新的更改。
重写这些方法以自定义回滚:
// 确定是否恢复先前状态
bool shouldRollback() => true;
// 指定要恢复的确切状态
AppState? rollbackState() => previousState;
默认非重入
OptimisticCommand防止同一操作的并发执行。使用nonReentrantKeyParams()允许对不同项目进行并行操作:
class SaveTodo extends AppAction with OptimisticCommand {
final String itemId;
SaveTodo(this.itemId);
// 允许SaveTodo('A')和SaveTodo('B')同时运行
// 但防止两个SaveTodo('A')一起运行
Object? nonReentrantKeyParams() => itemId;
// ... 其余实现
}
在UI中检查操作是否进行中:
if (context.isWaiting(SaveTodo)) {
return CircularProgressIndicator();
}
与其他混合器组合
- 与Retry:仅
sendCommandToServer重试;乐观UI保持稳定 - 与CheckInternet:离线时不应用乐观状态
OptimisticSync
用于快速用户交互(切换喜欢、开关、滑块),其中只有最终值重要,中间状态可丢弃。
切换示例
class ToggleLike extends AppAction with OptimisticSync<AppState, bool> {
final String itemId;
ToggleLike(this.itemId);
// 允许对不同项目的并发操作
Object? optimisticSyncKeyParams() => itemId;
// 应用乐观值(切换当前值)
bool valueToApply() => !state.items[itemId].liked;
// 应用乐观更改到状态
AppState applyOptimisticValueToState(AppState state, bool isLiked)
=> state.copy(items: state.items.setLiked(itemId, isLiked));
// 从状态提取当前值
bool getValueFromState(AppState state) => state.items[itemId].liked;
// 发送到服务器
Future<Object?> sendValueToServer(Object? value) async
=> await api.setLiked(itemId, value);
// 可选:应用服务器响应到状态
AppState? applyServerResponseToState(AppState state, Object serverResponse)
=> state.copy(items: state.items.setLiked(itemId, serverResponse as bool));
// 可选:处理完成/错误
Future<AppState?> onFinish(Object? error) async {
if (error != null) {
// 失败时从服务器重新加载
var reloaded = await api.getItem(itemId);
return state.copy(items: state.items.update(itemId, reloaded));
}
return null;
}
}
合并如何工作
多个快速更改合并为最小服务器请求:
- 用户快速点击喜欢按钮5次
- UI立即每次更新(切换、切换、切换…)
- 仅一个服务器请求发送最终状态
- 如果在飞行请求期间状态更改,则后续请求发送新最终值
OptimisticSyncWithPush
当应用接收实时服务器更新(WebSockets、Firebase),跨多个设备修改共享数据时使用。
与OptimisticSync的关键区别
- 每个本地调度递增
localRevision计数器 - 服务器推送不递增
localRevision - 后续逻辑比较版本而不是仅值
- 过时推送自动忽略
实现
class ToggleLike extends AppAction with OptimisticSyncWithPush<AppState, bool> {
final String itemId;
ToggleLike(this.itemId);
Object? optimisticSyncKeyParams() => itemId;
bool valueToApply() => !state.items[itemId].liked;
AppState applyOptimisticValueToState(AppState state, bool isLiked)
=> state.copy(items: state.items.setLiked(itemId, isLiked));
bool getValueFromState(AppState state) => state.items[itemId].liked;
// 从状态读取服务器版本
int? getServerRevisionFromState(Object? key)
=> state.items[key as String].serverRevision;
AppState? applyServerResponseToState(AppState state, Object serverResponse)
=> state.copy(items: state.items.setLiked(itemId, serverResponse as bool));
Future<Object?> sendValueToServer(Object? value) async {
// 在await之前获取本地版本
int localRev = localRevision();
var response = await api.setLiked(itemId, value, localRev: localRev);
// 响应后记录服务器版本
informServerRevision(response.serverRev);
return response.liked;
}
}
ServerPush混合器
使用自动过时检测处理传入服务器推送:
class PushLikeUpdate extends AppAction with ServerPush<AppState> {
final String itemId;
final bool liked;
final int serverRev;
PushLikeUpdate({
required this.itemId,
required this.liked,
required this.serverRev,
});
// 链接到相应的OptimisticSyncWithPush操作
Type associatedAction() => ToggleLike;
Object? optimisticSyncKeyParams() => itemId;
int serverRevision() => serverRev;
int? getServerRevisionFromState(Object? key)
=> state.items[key as String].serverRevision;
AppState? applyServerPushToState(AppState state, Object? key, int serverRevision)
=> state.copy(
items: state.items.update(
key as String,
(item) => item.copy(liked: liked, serverRevision: serverRevision),
),
);
}
如果传入serverRevision ≤ 当前已知版本,推送自动忽略。这防止较旧服务器状态覆盖较新状态。
版本跟踪的数据模型
在数据模型中存储服务器版本:
class Item {
final bool liked;
final int? serverRevision;
Item({required this.liked, this.serverRevision});
Item copy({bool? liked, int? serverRevision}) => Item(
liked: liked ?? this.liked,
serverRevision: serverRevision ?? this.serverRevision,
);
}
通知用户回滚
要通知用户回滚发生时,在错误处理中使用UserException:
class SaveTodo extends AppAction with OptimisticCommand {
// ... 所需方法 ...
Future<Object?> sendCommandToServer(Object? value) async {
try {
return await saveTodo(newTodo);
} catch (e) {
// 抛出UserException以在回滚后显示对话框
throw UserException('保存失败。您的更改已恢复。').addCause(e);
}
}
}
或使用OptimisticSync的onFinish:
Future<AppState?> onFinish(Object? error) async {
if (error != null) {
// 调度通知操作
dispatch(UserExceptionAction('更新失败。正在恢复...'));
// 从服务器重新加载正确状态
var reloaded = await api.getItem(itemId);
return state.copy(items: state.items.update(itemId, reloaded));
}
return null;
}
选择正确的混合器
| 场景 | 混合器 |
|---|---|
| 创建/删除/提交操作 | OptimisticCommand |
| 切换开关、喜欢按钮 | OptimisticSync |
| 滑块、快速输入更改 | OptimisticSync |
| 多设备实时同步 | OptimisticSyncWithPush + ServerPush |
参考
文档中的URL:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/advanced-actions/optimistic-mixins
- https://asyncredux.com/flutter/advanced-actions/action-mixins
- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer
- https://asyncredux.com/flutter/advanced-actions/control-mixins
- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions
- https://asyncredux.com/flutter/basics/async-actions
- https://asyncredux.com/flutter/basics/failed-actions