使用 Result 对象进行错误处理
Dart 提供了一个内置的错误处理机制,能够抛出和捕获异常。
如 错误处理文档中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法不需要声明它们,调用方法也不需要捕获它们。
这可能导致异常未得到妥善处理的情况。在大型项目中,开发者可能会忘记捕获异常,而不同的应用层和组件可能会抛出未记录的异常。这可能导致错误和崩溃。
在本指南中,您将了解此限制以及如何使用 *Result* 模式来缓解此问题。
Flutter 应用中的错误流程
#遵循 Flutter 架构指南的应用通常由 ViewModel、Repository 和 Service 等部分组成。当这些组件中的函数失败时,它应该将错误通知给调用组件。
通常,这是通过异常来完成的。例如,API 客户端服务在与远程服务器通信失败时可能会抛出 `HttpError` 异常。调用组件,例如 Repository,将必须捕获此异常,或忽略它并让调用 ViewModel 来处理。
这可以在以下示例中观察到。考虑这些类:
- 一个服务 `ApiClientService`,它执行对远程服务的 API 调用。
- 一个 Repository `UserProfileRepository`,它提供由 `ApiClientService` 提供的 `UserProfile`。
- 一个 ViewModel `UserProfileViewModel`,它使用 `UserProfileRepository`。
`ApiClientService` 包含一个方法 `getUserProfile`,该方法在某些情况下会抛出异常。
- 如果响应代码不是 200,该方法会抛出 `HttpException`。
- 如果响应格式不正确,JSON 解析方法会抛出异常。
- HTTP 客户端可能会因网络问题而抛出异常。
以下代码测试了各种可能的异常。
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 Client 的值。
class UserProfileRepository {
// ···
Future<UserProfile> getUserProfile() async {
return await _apiClientService.getUserProfile();
}
}
最后,`UserProfileViewModel` 应捕获所有异常并处理错误。
这可以通过将对 `UserProfileRepository` 的调用包装在 `try-catch` 中来完成。
class UserProfileViewModel extends ChangeNotifier {
// ···
Future<void> load() async {
try {
_userProfile = await userProfileRepository.getUserProfile();
notifyListeners();
} on Exception catch (exception) {
// handle exception
}
}
}
实际上,开发者可能会忘记正确捕获异常,从而得到以下代码。它能够编译和运行,但如果发生前面提到的任何异常,就会崩溃。
class UserProfileViewModel extends ChangeNotifier {
// ···
Future<void> load() async {
_userProfile = await userProfileRepository.getUserProfile();
notifyListeners();
}
}
您可以尝试通过记录 `ApiClientService` 来解决此问题,警告其可能抛出的异常。但是,由于 ViewModel 不直接使用该服务,因此代码库中的其他开发者可能会忽略此信息。
使用 Result 模式
#抛出异常的替代方法是将函数输出包装在 `Result` 对象中。
当函数成功运行时,`Result` 包含返回的值。但是,如果函数未成功完成,`Result` 对象将包含错误。
`Result` 是一个 sealed
类,它可以继承 `Ok` 或 `Error` 类。使用 `Ok` 子类返回成功的值,使用 `Error` 子类返回捕获的错误。
以下代码显示了一个简化的 `Result` 类,仅用于演示目的。完整的实现位于本页末尾。
/// 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`。
class ApiClientService {
// ···
Future<Result<UserProfile>> getUserProfile() async {
// ···
}
}
它返回一个包含 `UserProfile` 的 `Result` 对象,而不是直接返回 `UserProfile`。
为了方便使用 `Result` 类,它包含两个命名构造函数 `Result.ok` 和 `Result.error`。根据期望的输出使用它们来构造 `Result`。此外,捕获代码抛出的任何异常并将其包装到 `Result` 对象中。
例如,此处 `getUserProfile()` 方法已更改为使用 `Result` 类。
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();
}
}
}
原始的 return 语句被替换为使用 `Result.ok` 返回值的语句。`throw HttpException()` 被替换为返回 `Result.error(HttpException())` 的语句,将错误包装到 `Result` 中。此外,该方法被包装在一个 `try-catch` 块中,以将 HTTP 客户端或 JSON 解析器抛出的任何异常捕获到 `Result.error` 中。
Repository 类也需要修改,不再直接返回 `UserProfile`,而是返回 `Result<UserProfile>`。
Future<Result<UserProfile>> getUserProfile() async {
return await _apiClientService.getUserProfile();
}
解包 Result 对象
#现在 ViewModel 不直接接收 `UserProfile`,而是接收一个包含 `UserProfile` 的 `Result`。
这强制实现 ViewModel 的开发者解包 `Result` 以获取 `UserProfile`,并避免出现未捕获的异常。
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<UserProfile>` 的情况下,使用 `value` 属性获取值。
在 `Error<UserProfile>` 的情况下,使用 `error` 属性获取错误对象。
改进控制流
#将代码包装在 `try-catch` 块中可确保捕获抛出的异常,而不是将其传播到代码的其他部分。
考虑以下代码。
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` 模式进行改进。
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 语句解包它们。
您可以在下方找到 `Result` 类的完整实现,该实现是在 Compass App 示例中为 Flutter 架构指南实现的。
/// 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)';
}