/// Sealed response type returned by every [ApiClient] call. /// /// Use Dart 3 exhaustive pattern-matching so both outcomes are always handled: /// ```dart /// final result = await ApiClient.get( /// url: '/users/me', /// fromJson: (j) => User.fromJson(j as Map), /// ); /// /// switch (result) { /// case ApiSuccess(:final data): /// print('Got user: ${data.name}'); /// case ApiFailure(:final code, :final message): /// print('Error $code: $message'); /// } /// ``` sealed class ApiResponse {} /// Represents a successful API call whose response body was parsed into [data] /// via the caller-supplied `fromJson` function. final class ApiSuccess extends ApiResponse { /// The parsed response payload. final T data; ApiSuccess(this.data); } /// Represents a failed API call, carrying an optional HTTP status [code] and a /// human-readable [message]. /// /// Failure sources include: /// * No internet connectivity → `code: null` /// * Connection / receive timeout → `code: null` or server code /// * HTTP error (non-2xx or `isSuccess` predicate returned `false`) → HTTP code /// * Response JSON parsing error → HTTP code /// * Request cancellation via `ApiClient.cancel()` → `code: -1` /// * Client not initialized (`ApiClientNotInitializedException`) → `code: null` /// * Unknown / unexpected error → `code: null` final class ApiFailure extends ApiResponse { /// HTTP status code when available, or `null` when the failure happened /// before / after the network layer (e.g. no connectivity, parsing error). /// /// A synthetic value of `-1` is used for cancellations so callers can /// distinguish them from real HTTP errors without string-matching. final int? code; /// Human-readable description of the failure suitable for display. final String message; ApiFailure({this.code, required this.message}); }