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 客戶端可能會因網路問題而丟擲異常。

以下程式碼測試了各種可能的異常。

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 Client 的值。

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` 來解決此問題,警告其可能丟擲的異常。但是,由於 ViewModel 不直接使用該服務,因此程式碼庫中的其他開發者可能會忽略此資訊。

使用 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` 的 `Result` 物件,而不是直接返回 `UserProfile`。

為了方便使用 `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();
    }
  }
}

原始的 return 語句被替換為使用 `Result.ok` 返回值的語句。`throw HttpException()` 被替換為返回 `Result.error(HttpException())` 的語句,將錯誤包裝到 `Result` 中。此外,該方法被包裝在一個 `try-catch` 塊中,以將 HTTP 客戶端或 JSON 解析器丟擲的任何異常捕獲到 `Result.error` 中。

Repository 類也需要修改,不再直接返回 `UserProfile`,而是返回 `Result<UserProfile>`。

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

解包 Result 物件

#

現在 ViewModel 不直接接收 `UserProfile`,而是接收一個包含 `UserProfile` 的 `Result`。

這強制實現 ViewModel 的開發者解包 `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<UserProfile>` 的情況下,使用 `value` 屬性獲取值。

在 `Error<UserProfile>` 的情況下,使用 `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 語句解包它們。

您可以在下方找到 `Result` 類的完整實現,該實現是在 Compass App 示例中為 Flutter 架構指南實現的。

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)';
}