Dart 提供内置的错误处理机制,能够抛出和捕获异常。

如《错误处理文档》中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法无需声明它们,调用方法也不需要捕获它们。

这可能导致异常未被正确处理的情况。在大型项目中,开发者可能会忘记捕获异常,并且不同的应用程序层和组件可能会抛出未被文档记录的异常。这可能导致错误和崩溃。

在本指南中,你将了解此限制以及如何使用 Result 模式 来缓解它。

Flutter 应用程序中的错误流

#

遵循Flutter 架构指南的应用程序通常由视图模型 (view models)、仓库 (repositories) 和服务 (services) 等部分组成。当其中一个组件中的函数失败时,它应该将错误传达给调用组件。

通常,这是通过异常来完成的。例如,一个无法与远程服务器通信的 API 客户端服务可能会抛出一个 HTTP 错误异常 (HTTP Error Exception)。调用组件,例如一个仓库 (Repository),必须捕获此异常,或者忽略它并让调用视图模型 (view model) 处理它。

这可以在以下示例中观察到。考虑以下类:

  • 一个服务 `ApiClientService`,它向远程服务执行 API 调用。
  • 一个仓库 `UserProfileRepository`,提供由 `ApiClientService` 提供的 `UserProfile`。
  • 一个视图模型 `UserProfileViewModel`,使用 `UserProfileRepository`。

`ApiClientService` 包含一个 `getUserProfile` 方法,该方法在某些情况下会抛出异常:

  • 如果响应代码不是 200,该方法会抛出 `HttpException`。
  • 如果响应格式不正确,JSON 解析方法会抛出异常。
  • HTTP 客户端可能由于网络问题抛出异常。

以下代码测试各种可能的异常:

dart
class ApiClientService {
  // ···

  Future<UserProfile> getUserProfile() async {
    try {
      final request = await client.get(_host, _port, '/user');
      final response = await request.close();
      if (response.statusCode == 200) {
        final stringData = await response.transform(utf8.decoder).join();
        return UserProfile.fromJson(jsonDecode(stringData));
      } else {
        throw const HttpException('Invalid response');
      }
    } finally {
      client.close();
    }
  }
}

`UserProfileRepository` 不需要处理来自 `ApiClientService` 的异常。在此示例中,它只是返回来自 API 客户端的值。

dart
class UserProfileRepository {
  // ···

  Future<UserProfile> getUserProfile() async {
    return await _apiClientService.getUserProfile();
  }
}

最后,`UserProfileViewModel` 应该捕获所有异常并处理错误。

这可以通过将对 `UserProfileRepository` 的调用包装在 try-catch 块中来完成。

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  Future<void> load() async {
    try {
      _userProfile = await userProfileRepository.getUserProfile();
      notifyListeners();
    } on Exception catch (exception) {
      // handle exception
    }
  }
}

实际上,开发者可能会忘记正确捕获异常,并最终得到以下代码。它能够编译和运行,但如果出现前面提到的任何异常,则会崩溃。

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  Future<void> load() async {
    _userProfile = await userProfileRepository.getUserProfile();
    notifyListeners();
  }
}

你可以尝试通过文档化 `ApiClientService` 来解决此问题,警告它可能抛出的异常。然而,由于视图模型不直接使用该服务,在代码库中工作的其他开发者可能会错过此信息。

使用 Result 模式

#

抛出异常的替代方法是将函数输出包装在 `Result` 对象中。

当函数成功运行时,`Result` 包含返回的值。但是,如果函数未能成功完成,`Result` 对象将包含错误。

一个 `Result` 是一个 `sealed` 类,它可以是 `Ok` 或 `Error` 类的子类。使用子类 `Ok` 返回成功的值,使用子类 `Error` 返回捕获的错误。

以下代码显示了一个为演示目的而简化的 `Result` 类示例。完整的实现可在本页末尾找到。

dart
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
sealed class Result<T> {
  const Result();

  /// Creates an instance of Result containing a value
  factory Result.ok(T value) => Ok(value);

  /// Create an instance of Result containing an error
  factory Result.error(Exception error) => Error(error);
}

/// Subclass of Result for values
final class Ok<T> extends Result<T> {
  const Ok(this.value);

  /// Returned value in result
  final T value;
}

/// Subclass of Result for errors
final class Error<T> extends Result<T> {
  const Error(this.error);

  /// Returned error in result
  final Exception error;
}

在此示例中,`Result` 类使用泛型类型 `T` 来表示任何返回值,它可以是像 `String` 或 `int` 这样的 Dart 基本类型,也可以是像 `UserProfile` 这样的自定义类。

创建 `Result` 对象

#

对于使用 `Result` 类返回值的函数,该函数不再直接返回一个值,而是返回一个包含该值的 `Result` 对象。

例如,在 `ApiClientService` 中,`getUserProfile` 被修改为返回一个 `Result`。

dart
class ApiClientService {
  // ···

  Future<Result<UserProfile>> getUserProfile() async {
    // ···
  }
}

它不再直接返回 `UserProfile`,而是返回一个包含 `UserProfile` 的 `Result` 对象。

为了方便使用 `Result` 类,它包含两个命名构造函数:`Result.ok` 和 `Result.error`。根据所需的输出,使用它们来构造 `Result`。同时,捕获代码抛出的任何异常并将其包装到 `Result` 对象中。

例如,这里 `getUserProfile()` 方法已更改为使用 `Result` 类:

dart
class ApiClientService {
  // ···

  Future<Result<UserProfile>> getUserProfile() async {
    try {
      final request = await client.get(_host, _port, '/user');
      final response = await request.close();
      if (response.statusCode == 200) {
        final stringData = await response.transform(utf8.decoder).join();
        return Result.ok(UserProfile.fromJson(jsonDecode(stringData)));
      } else {
        return const Result.error(HttpException('Invalid response'));
      }
    } on Exception catch (exception) {
      return Result.error(exception);
    } finally {
      client.close();
    }
  }
}

原始的返回语句被替换为使用 `Result.ok` 返回值的语句。`throw HttpException()` 被替换为返回 `Result.error(HttpException())` 的语句,将错误包装到 `Result` 中。此外,该方法被 `try-catch` 块包装,以捕获 HTTP 客户端或 JSON 解析器抛出的任何异常并将其封装到 `Result.error` 中。

仓库类也需要修改,它不再直接返回 `UserProfile`,而是返回 `Result`。

dart
Future<Result<UserProfile>> getUserProfile() async {
  return await _apiClientService.getUserProfile();
}

解包 Result 对象

#

现在视图模型不再直接接收 `UserProfile`,而是接收一个包含 `UserProfile` 的 `Result`。

这强制实现视图模型的开发者解包 `Result` 以获取 `UserProfile`,并避免出现未捕获的异常。

dart
class UserProfileViewModel extends ChangeNotifier {
  // ···

  UserProfile? userProfile;

  Exception? error;

  Future<void> load() async {
    final result = await userProfileRepository.getUserProfile();
    switch (result) {
      case Ok<UserProfile>():
        userProfile = result.value;
      case Error<UserProfile>():
        error = result.error;
    }
    notifyListeners();
  }
}

`Result` 类是使用 `sealed` 类实现的,这意味着它只能是 `Ok` 或 `Error` 类型。这使得代码可以使用以下方式评估结果:
switch 语句或表达式.

在 `Ok` 的情况下,使用 `value` 属性获取值。

在 `Error` 的情况下,使用 `error` 属性获取错误对象。

改进控制流

#

将代码包装在 `try-catch` 块中可确保捕获抛出的异常,并且不会传播到代码的其他部分。

考虑以下代码。

dart
class UserProfileRepository {
  // ···

  Future<UserProfile> getUserProfile() async {
    try {
      return await _apiClientService.getUserProfile();
    } catch (e) {
      try {
        return await _databaseService.createTemporaryUser();
      } catch (e) {
        throw Exception('Failed to get user profile');
      }
    }
  }
}

在此方法中,`UserProfileRepository` 尝试使用 `ApiClientService` 获取 `UserProfile`。如果失败,它会尝试在 `DatabaseService` 中创建一个临时用户。

因为这两种服务方法都可能失败,所以代码必须在这两种情况下都捕获异常。

这可以使用 `Result` 模式进行改进:

dart
Future<Result<UserProfile>> getUserProfile() async {
  final apiResult = await _apiClientService.getUserProfile();
  if (apiResult is Ok) {
    return apiResult;
  }

  final databaseResult = await _databaseService.createTemporaryUser();
  if (databaseResult is Ok) {
    return databaseResult;
  }

  return Result.error(Exception('Failed to get user profile'));
}

在此代码中,如果 `Result` 对象是 `Ok` 实例,则函数返回该对象;否则,它返回 `Result.Error`。

整合所有概念

#

在本指南中,你学习了如何使用 `Result` 类来返回结果值。

主要收获是:

  • `Result` 类强制调用方法检查错误,从而减少由未捕获异常引起的错误数量。
  • 与 try-catch 块相比,`Result` 类有助于改进控制流。
  • `Result` 类是 `sealed` 的,并且只能返回 `Ok` 或 `Error` 实例,这允许代码使用 switch 语句解包它们。

下面你可以找到在《指南针应用示例》中为Flutter 架构指南实现的完整 `Result` 类。

dart
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Error] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
///
/// Evaluate the result using a switch statement:
/// ```dart
/// switch (result) {
///   case Ok(): {
///     print(result.value);
///   }
///   case Error(): {
///     print(result.error);
///   }
/// }
/// ```
sealed class Result<T> {
  const Result();

  /// Creates a successful [Result], completed with the specified [value].
  const factory Result.ok(T value) = Ok._;

  /// Creates an error [Result], completed with the specified [error].
  const factory Result.error(Exception error) = Error._;
}

/// A successful [Result] with a returned [value].
final class Ok<T> extends Result<T> {
  const Ok._(this.value);

  /// The returned value of this result.
  final T value;

  @override
  String toString() => 'Result<$T>.ok($value)';
}

/// An error [Result] with a resulting [error].
final class Error<T> extends Result<T> {
  const Error._(this.error);

  /// The resulting error of this result.
  final Exception error;

  @override
  String toString() => 'Result<$T>.error($error)';
}