name: asyncredux-user-exceptions
description: 使用 UserException 处理用户面对的错误。涵盖了从动作中抛出 UserException、设置 UserExceptionDialog、使用 onShowUserExceptionDialog 自定义错误对话框,以及使用 UserExceptionAction 进行非中断错误显示。
AsyncRedux 中的 UserException
UserException 是一种特殊的错误类型,用于用户面对的错误,应该显示给用户而不是作为 bug 记录。这些代表用户可以解决或应该被告知的问题。
从动作中抛出 UserException
当动作遇到用户面对的错误时,抛出 UserException:
class TransferMoney extends AppAction {
final double amount;
TransferMoney(this.amount);
AppState? reduce() {
if (amount == 0) {
throw UserException('您不能转账零金额。');
}
return state.copy(cash: state.cash - amount);
}
}
对于带有验证的异步动作:
class SaveUser extends AppAction {
final String name;
SaveUser(this.name);
Future<AppState?> reduce() async {
if (name.length < 4)
throw UserException('姓名必须至少包含 4 个字母。');
await saveUser(name);
return null;
}
}
将错误转换为 UserException
使用 addCause() 在显示用户友好消息的同时保留原始错误:
class ConvertAction extends AppAction {
final String text;
ConvertAction(this.text);
Future<AppState?> reduce() async {
try {
var value = int.parse(text);
return state.copy(counter: value);
} catch (error) {
throw UserException('请输入有效的数字')
.addCause(error);
}
}
}
设置 UserExceptionDialog
将您的首页用 UserExceptionDialog 包裹在 StoreProvider 和 MaterialApp 下方:
Widget build(context) {
return StoreProvider<AppState>(
store: store,
child: MaterialApp(
home: UserExceptionDialog<AppState>(
child: MyHomePage(),
),
),
);
}
如果省略 onShowUserExceptionDialog 参数,会显示一个默认对话框,包含错误消息和确定按钮。
自定义错误对话框
使用 onShowUserExceptionDialog 创建自定义错误对话框:
UserExceptionDialog<AppState>(
onShowUserExceptionDialog: (BuildContext context, UserException exception) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('错误'),
content: Text(exception.message ?? '发生错误'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('确定'),
),
],
),
);
},
child: MyHomePage(),
)
对于非标准错误呈现(如 Snackbar 或横幅),可以通过在自定义实现中访问 didUpdateWidget 方法来修改行为。
用于非中断错误的 UserExceptionAction
使用 UserExceptionAction 显示错误对话框而不抛出异常或停止动作执行:
// 显示错误对话框而不导致动作失败
dispatch(UserExceptionAction('请输入有效的数字'));
这在您想通知用户问题但在动作执行过程中继续时很有用:
class ConvertAction extends AppAction {
final String text;
ConvertAction(this.text);
Future<AppState?> reduce() async {
var value = int.tryParse(text);
if (value == null) {
// 显示对话框但动作继续
dispatch(UserExceptionAction('无效数字,使用默认值'));
value = 0;
}
return state.copy(counter: value);
}
}
使用 Mixin 进行可重用的错误处理
创建 mixin 以标准化跨动作的 UserException 转换:
mixin ShowUserException on AppAction {
String getErrorMessage();
Object? wrapError(Object error, StackTrace stackTrace) {
return UserException(getErrorMessage()).addCause(error);
}
}
class ConvertAction extends AppAction with ShowUserException {
final String text;
ConvertAction(this.text);
@override
String getErrorMessage() => '请输入有效的数字。';
Future<AppState?> reduce() async {
var value = int.parse(text); // 任何错误都会转换为 UserException
return state.copy(counter: value);
}
}
使用 GlobalWrapError 进行全局错误处理
统一处理所有动作中的第三方或框架错误:
var store = Store<AppState>(
initialState: AppState.initialState(),
globalWrapError: MyGlobalWrapError(),
);
class MyGlobalWrapError extends GlobalWrapError {
@override
Object? wrap(Object error, StackTrace stackTrace, ReduxAction<dynamic> action) {
if (error is PlatformException &&
error.code == 'Error performing get') {
return UserException('检查您的互联网连接')
.addCause(error);
}
// 对于其他情况,返回错误不变
return error;
}
}
处理顺序:动作的 wrapError() -> GlobalWrapError -> ErrorObserver
错误队列
抛出的 UserException 实例存储在 store 的专用错误队列中。队列由 UserExceptionDialog 消费以显示错误消息。您可以在 Store 构造函数中配置最大队列容量。
在 Widget 中检查失败的动作
使用这些方法检查动作失败状态并内联显示错误:
Widget build(BuildContext context) {
if (context.isFailed(SaveUserAction)) {
var exception = context.exceptionFor(SaveUserAction);
return Column(
children: [
Text('失败: ${exception?.message}'),
ElevatedButton(
onPressed: () {
context.clearExceptionFor(SaveUserAction);
context.dispatch(SaveUserAction(name));
},
child: Text('重试'),
),
],
);
}
return Text('用户保存成功');
}
注意:当动作重新分发时,错误状态会自动清除,因此通常在重试前不需要手动清理。
测试 UserExceptions
测试动作是否正确抛出 UserException:
test('应该为无效输入抛出 UserException', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var status = await store.dispatchAndWait(TransferMoney(0));
expect(status.isCompletedFailed, isTrue);
var error = status.wrappedError;
expect(error, isA<UserException>());
expect((error as UserException).message, '您不能转账零金额。');
});
使用错误队列测试多个异常:
test('应该收集多个 UserExceptions', () async {
var store = Store<AppState>(initialState: AppState.initialState());
await store.dispatchAndWaitAll([
InvalidAction1(),
InvalidAction2(),
InvalidAction3(),
]);
var errors = store.errors;
expect(errors.length, 3);
expect(errors[0].message, '第一个错误消息');
});
参考文献
文档中的 URL:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions
- https://asyncredux.com/flutter/basics/failed-actions
- https://asyncredux.com/flutter/testing/testing-user-exceptions
- https://asyncredux.com/flutter/basics/wait-fail-succeed
- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer
- https://asyncredux.com/flutter/basics/store