AsyncRedux用户异常处理技能Skill asyncredux-user-exceptions

AsyncRedux 中的 UserException 技能用于在 Flutter 应用中处理用户面对的错误,包括从动作中抛出异常、设置错误对话框、自定义错误显示和非中断错误通知。关键词:AsyncRedux、UserException、错误处理、Flutter、状态管理、移动开发、用户体验。

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

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 包裹在 StoreProviderMaterialApp 下方:

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: