使用 Result 对象进行错误处理
Dart 提供了内置的错误处理机制,能够抛出和捕获异常。
如错误处理文档中所述,Dart 的异常是未处理的异常。这意味着抛出异常的方法不需要声明它们,调用方法也不需要捕获它们。
这可能导致异常未被正确处理的情况。在大项目中,开发人员可能会忘记捕获异常,不同的应用程序层和组件可能会抛出未记录的异常。这可能导致错误和崩溃。
在本指南中,您将了解此限制以及如何使用 result 模式来减轻它。
Flutter 应用中的错误流
#遵循Flutter 架构指南 的应用程序通常由视图模型、存储库和服务等部分组成。当这些组件中的一个函数失败时,它应该将错误传达给调用组件。
通常,这是通过异常来完成的。例如,API 客户端服务未能与远程服务器通信可能会抛出 HTTP 错误异常。调用组件(例如存储库)必须捕获此异常或忽略它并让调用视图模型处理它。
这可以在以下示例中观察到。考虑以下类:
- 服务
ApiClientService
执行对远程服务的 API 调用。 - 存储库
UserProfileRepository
提供由ApiClientService
提供的UserProfile
。 - 视图模型
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 客户端的值。
class UserProfileRepository {
// ···
Future<UserProfile> getUserProfile() async {
return await _apiClientService.getUserProfile();
}
}
最后,UserProfileViewModel
应该捕获所有异常并处理错误。
这可以通过用 try-catch 包装对 UserProfileRepository
的调用来完成:
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
来解决此问题,警告它可能抛出的可能的异常。但是,由于视图模型不直接使用服务,因此在代码库中工作的其他开发人员可能会错过此信息。
使用 result 模式
#抛出异常的替代方法是用 Result
对象包装函数输出。
当函数成功运行时,Result
包含返回值。但是,如果函数未成功完成,Result
对象包含错误。
Result
是一个 sealed
类,它可以是 Ok
子类或 Error
类。使用子类 Ok
返回成功值,使用子类 Error
返回捕获的错误。
以下代码显示了一个 Result
类的示例,为了演示目的,它已被简化。完整的实现位于此页的末尾。
/// 简化错误处理的实用程序类。
///
/// 从函数返回 [Result] 以指示成功或失败。
///
/// [Result] 要么是包含类型为 [T] 的值的 [Ok],
/// 要么是包含 [Exception] 的 [Error]。
///
/// 使用 [Result.ok] 创建一个包含类型为 [T] 的值的成功结果。
/// 使用 [Result.error] 创建一个包含 [Exception] 的错误结果。
sealed class Result<T> {
const Result();
/// 创建包含值的 Result 实例
factory Result.ok(T value) => Ok(value);
/// 创建包含错误的 Result 实例
factory Result.error(Exception error) => Error(error);
}
/// 值的 Result 子类
final class Ok<T> extends Result<T> {
const Ok(this.value);
/// 结果中的返回值
final T value;
}
/// 错误的 Result 子类
final class Error<T> extends Result<T> {
const Error(this.error);
/// 结果中的返回错误
final Exception error;
}
在此示例中,Result
类使用泛型类型 T
来表示任何返回值,它可以是像 String
或 int
这样的 Dart 原生类型,也可以是像 UserProfile
这样的自定义类。
创建 Result
对象
#对于使用 Result
类返回值的函数,函数不返回值,而是返回包含值的 Result
对象。
例如,在 ApiClientService
中,getUserProfile
被更改为返回 Result
:
class ApiClientService {
// ···
Future<Result<UserProfile>> getUserProfile() async {
// ···
}
}
它不直接返回 UserProfile
,而是返回包含 UserProfile
的 Result
对象。
为了方便使用 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
中。
存储库类也需要修改,它不再直接返回 UserProfile
,而是返回 Result<UserProfile>
。
Future<Result<UserProfile>> getUserProfile() async {
return await _apiClientService.getUserProfile();
}
解包 Result 对象
#现在视图模型不直接接收 UserProfile
,而是接收包含 UserProfile
的 Result
。
这强制实现视图模型的开发人员解包 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
类,用于Flutter 架构指南。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。