名称: asyncredux-error-handling
描述: 实现动作的全面错误处理。涵盖了动作级别的 wrapError() 方法进行错误包装、全局错误转换的 GlobalWrapError、日志记录/监控的 ErrorObserver,以及错误处理流程(before → reduce → after)。
AsyncRedux 中的错误处理
AsyncRedux 提供了一个全面的错误处理系统,具有多个层次:动作级别包装、全局错误转换和用于日志记录/监控的错误观察。
错误流和动作生命周期
当错误在动作执行期间发生时:
- 如果
before()抛出错误,则reducer不执行,状态保持不变 - 如果
reduce()抛出错误,则执行停止,状态未修改 after()方法 始终 运行,即使在错误发生时(类似于finally块)
处理顺序: wrapError() → GlobalWrapError → ErrorObserver
从动作抛出错误
动作可以使用 throw 抛出错误。当错误被抛出时,reducer 停止且状态不修改:
class TransferMoney extends AppAction {
final double amount;
TransferMoney(this.amount);
AppState? reduce() {
if (amount == 0) {
throw UserException('You cannot transfer zero money.');
}
return state.copy(cash: state.cash - amount);
}
}
UserException 用于面向用户的错误
UserException 是一个内置类,用于用户可以理解和潜在修复的错误(而非代码错误):
class SaveUser extends AppAction {
final String name;
SaveUser(this.name);
Future<AppState?> reduce() async {
if (name.length < 4)
throw UserException('Name must have 4 letters.');
await saveUser(name);
return null;
}
}
当 UserException 被抛出时,它被添加到存储中的一个特殊错误队列中,并可以通过 UserExceptionDialog 显示。
显示 UserExceptions
将您的主页包装在 UserExceptionDialog 中,位于 StoreProvider 和 MaterialApp 下方:
UserExceptionDialog<AppState>(
onShowUserExceptionDialog: (context, exception) => showDialog(...),
child: MyHomePage(),
)
动作级别错误包装使用 wrapError()
wrapError() 方法作为整个动作的 catch 块。它接收原始错误和堆栈跟踪,并且必须返回:
- 修改后的错误(以转换错误)
null(以抑制/禁用错误)- 未更改的错误(以传递它)
class LogoutAction extends AppAction {
@override
Object? wrapError(Object error, StackTrace stackTrace) {
return LogoutError("Logout failed", cause: error);
}
Future<AppState?> reduce() async {
await authService.logout();
return state.copy(user: null);
}
}
Mixin 模式用于可重用的错误处理
创建 mixin 以在多个动作中实现一致的错误转换:
mixin ShowUserException on AppAction {
String getErrorMessage();
@override
Object? wrapError(Object error, StackTrace stackTrace) {
return UserException(getErrorMessage()).addCause(error);
}
}
class LoadDataAction extends AppAction with ShowUserException {
@override
String getErrorMessage() => 'Failed to load data. Please try again.';
Future<AppState?> reduce() async {
var data = await api.loadData();
return state.copy(data: data);
}
}
抑制错误
从 wrapError() 返回 null 以抑制错误而不进一步传播:
@override
Object? wrapError(Object error, StackTrace stackTrace) {
if (error is CancelledException) {
return null; // 静默忽略取消
}
return error;
}
全局错误处理使用 GlobalWrapError
GlobalWrapError 集中处理所有动作错误。这对于转换第三方库错误(如 Firebase 或平台异常)很有用:
var store = Store<AppState>(
initialState: AppState.initialState(),
globalWrapError: MyGlobalWrapError(),
);
class MyGlobalWrapError extends GlobalWrapError {
@override
Object? wrap(Object error, StackTrace stackTrace, ReduxAction<AppState> action) {
// 转换平台异常为用户友好的消息
if (error is PlatformException && error.code == "Error performing get") {
return UserException('Check your internet connection').addCause(error);
}
// 转换 Firebase 错误
if (error is FirebaseException) {
return UserException('Service temporarily unavailable').addCause(error);
}
// 传递所有其他错误不变
return error;
}
}
从 GlobalWrapError.wrap() 返回 null 以全局抑制错误。
错误观察使用 ErrorObserver
ErrorObserver 接收所有错误及其相关动作和存储上下文。用于日志记录、监控或分析:
var store = Store<AppState>(
initialState: AppState.initialState(),
errorObserver: MyErrorObserver<AppState>(),
);
class MyErrorObserver<St> implements ErrorObserver<St> {
@override
bool observe(
Object error,
StackTrace stackTrace,
ReduxAction<St> action,
Store<St> store,
) {
// 记录错误
print("Error during ${action.runtimeType}: $error");
// 发送到崩溃报告服务
crashlytics.recordError(error, stackTrace);
// 返回 true 以重新抛出,false 以吞下
return true;
}
}
observe 方法返回:
true以重新抛出错误(默认行为)false以静默吞下错误
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('Please enter a valid number'));
return null; // 无状态更改
}
return state.copy(counter: value);
}
}
检查动作失败状态
使用 ActionStatus
在使用 dispatchAndWait() 后,检查状态:
var status = await store.dispatchAndWait(SaveAction());
if (status.isCompletedOk) {
Navigator.pop(context);
} else if (status.isCompletedFailed) {
var error = status.wrappedError;
print('Save failed: $error');
}
ActionStatus 属性:
isCompletedOk:动作完成无错误isCompletedFailed:动作遇到错误originalError:从before或reduce抛出的错误wrappedError:经过wrapError()转换后的错误
在 Widgets 中使用 isFailed
在 UI 中检查动作失败状态:
Widget build(BuildContext context) {
if (context.isFailed(LoadDataAction)) {
var exception = context.exceptionFor(LoadDataAction);
return Column(
children: [
Text('Error: ${exception?.message}'),
ElevatedButton(
onPressed: () => context.dispatch(LoadDataAction()),
child: Text('Retry'),
),
],
);
}
if (context.isWaiting(LoadDataAction)) {
return CircularProgressIndicator();
}
return DataWidget(data: context.state.data);
}
当动作再次被分发时,错误会自动清除。
手动清除错误:
context.clearExceptionFor(LoadDataAction);
测试错误处理
测试动作以预期错误失败:
test('action throws UserException for invalid input', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var status = await store.dispatchAndWait(SaveUser('abc')); // 太短
expect(status.isCompletedFailed, isTrue);
var error = status.wrappedError;
expect(error, isA<UserException>());
expect((error as UserException).msg, 'Name must have 4 letters.');
});
通过错误队列测试多个异常:
test('multiple actions accumulate errors', () 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].msg, 'First error message');
});
带错误处理的完整存储设置
var store = Store<AppState>(
initialState: AppState.initialState(),
globalWrapError: MyGlobalWrapError(),
errorObserver: MyErrorObserver<AppState>(),
actionObservers: [Log.printer(formatter: Log.verySimpleFormatter)],
);
class MyGlobalWrapError extends GlobalWrapError {
@override
Object? wrap(Object error, StackTrace stackTrace, ReduxAction<AppState> action) {
if (error is SocketException) {
return UserException('No internet connection').addCause(error);
}
return error;
}
}
class MyErrorObserver<St> implements ErrorObserver<St> {
@override
bool observe(Object error, StackTrace stackTrace, ReduxAction<St> action, Store<St> store) {
// 跳过记录 UserExceptions(它们是预期的)
if (error is! UserException) {
crashlytics.recordError(error, stackTrace);
}
return true;
}
}
参考资料
文档的 URL:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions
- https://asyncredux.com/flutter/advanced-actions/wrapping-the-reducer
- https://asyncredux.com/flutter/basics/failed-actions
- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer
- https://asyncredux.com/flutter/basics/store
- https://asyncredux.com/flutter/testing/testing-user-exceptions
- https://asyncredux.com/flutter/basics/wait-fail-succeed
- https://asyncredux.com/flutter/miscellaneous/logging
- https://asyncredux.com/flutter/advanced-actions/action-status