/// ───────────────────────────────────────────────────────────────────────── /// WHAT A SCREEN IS SHOWING, AS ONE VALUE /// /// Screens carried three or four independent booleans — `_firstLoadDone`, /// `_fetching`, `_failed`, plus a list that might be empty — and every screen /// combined them slightly differently. That is how a page ends up showing an /// empty state during a refresh, or a spinner over stale data, or nothing at /// all when a request fails. /// /// One value, and the combinations that cannot happen are unrepresentable. /// /// ── Empty is not a failure, and unavailable is neither ── /// /// Three answers a rider acts on differently, which were all `[]` before: /// /// * **[LoadEmpty]** — the hub has given him nothing today. Wait. /// * **[LoadFailure]** — the request did not complete. Retry. /// * **[LoadUnavailable]** — his line of work has no endpoint behind it. Neither /// waiting nor retrying will help, and telling him to do either is a lie. /// See `ServiceProfile.hasBookingsEndpoint`. /// ───────────────────────────────────────────────────────────────────────── library; /// Why a load did not produce data. Kept separate from the HTTP status because /// the rider's next action is what differs, not the number. enum LoadFailureKind { /// No usable connection, a timeout, or a socket that died mid-flight. offline, /// The session is over. The shell signs the rider out; the screen should not /// offer a retry that cannot succeed. unauthorized, /// Too many requests. Retrying immediately makes it worse. rateLimited, /// The server answered and refused, or answered with something unreadable. server; /// Whether offering "Try again" is honest. bool get isRetryable => this != LoadFailureKind.unauthorized; } /// The state of one screen's data. sealed class LoadState { const LoadState(); /// The data, when there is any. Null in every other state — so a screen that /// forgets to handle a case renders nothing rather than stale content. T? get valueOrNull => switch (this) { LoadData(:final value) => value, _ => null, }; bool get isLoading => this is LoadLoading; } /// First load, or a refresh with nothing to show yet. class LoadLoading extends LoadState { const LoadLoading(); } /// Data arrived and there is something in it. class LoadData extends LoadState { final T value; /// True while a refresh is running behind data that is already on screen. /// /// The distinction the old booleans lost: a pull-to-refresh must not blank /// the list it is refreshing. final bool refreshing; const LoadData(this.value, {this.refreshing = false}); } /// The request succeeded and the answer was "nothing today". class LoadEmpty extends LoadState { const LoadEmpty(); } /// The request did not complete. class LoadFailure extends LoadState { final LoadFailureKind kind; /// The server's own sentence where it gave one — it is more useful than /// anything this app can invent about a failure it did not cause. final String message; const LoadFailure(this.kind, {this.message = ''}); } /// There is no backend for this rider's line of work. /// /// Not an error and not an empty day: a capability the deployment does not have /// yet. Retrying cannot fix it and neither can waiting. class LoadUnavailable extends LoadState { final String reason; const LoadUnavailable(this.reason); }