AsyncRedux错误处理Skill asyncredux-error-handling

AsyncRedux 错误处理技能提供了在 Flutter 应用中实现全面错误处理的机制。该技能覆盖了动作级别的错误包装使用 wrapError() 方法、全局错误转换使用 GlobalWrapError、错误观察使用 ErrorObserver,以及完整的错误处理流程(before → reduce → after)。它帮助开发者有效管理错误,提高移动应用的稳定性、用户体验和可维护性,适用于移动应用开发。关键词:AsyncRedux、错误处理、Flutter、wrapError、GlobalWrapError、ErrorObserver、移动开发、动作生命周期、用户异常。

移动开发 0 次安装 0 次浏览 更新于 3/19/2026

名称: asyncredux-error-handling 描述: 实现动作的全面错误处理。涵盖了动作级别的 wrapError() 方法进行错误包装、全局错误转换的 GlobalWrapError、日志记录/监控的 ErrorObserver,以及错误处理流程(before → reduce → after)。

AsyncRedux 中的错误处理

AsyncRedux 提供了一个全面的错误处理系统,具有多个层次:动作级别包装、全局错误转换和用于日志记录/监控的错误观察。

错误流和动作生命周期

当错误在动作执行期间发生时:

  1. 如果 before() 抛出错误,则reducer不执行,状态保持不变
  2. 如果 reduce() 抛出错误,则执行停止,状态未修改
  3. after() 方法 始终 运行,即使在错误发生时(类似于 finally 块)

处理顺序: wrapError()GlobalWrapErrorErrorObserver

从动作抛出错误

动作可以使用 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 中,位于 StoreProviderMaterialApp 下方:

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:从 beforereduce 抛出的错误
  • 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: