Don't report a failure nobody was there to see
Leaving the app for a while and coming back to it greeted you with a five-second read timeout. Work started before the screen went away keeps running -- a build being followed, a download, the manifest poll -- and when the device sleeps or the link drops, that work fails and reported itself. The message said nothing about the server and there was nothing left to do about it, because the thing it described was over before it was read. Every catch now goes through one `failure(e)`, which answers null while the screen is not resumed. Null means *clear*, never *leave*, at every site: a dropped failure that left the card alone would leave a spinner up for an operation that has already stopped. `setProject` joins `setComponent` so both levels are written the same way and a caller can hand the answer over whichever it is. That is only safe because the resume re-read can now run from any state. It was guarded on the list being loaded, which meant a failure ruled out the one thing that would have replaced it -- a card that had gone red stayed red until somebody found the Refresh button. Arriving and resuming are also one event now rather than a LaunchedEffect beside a resume effect, with `loadingList` claimed before the coroutine launches so the two cannot stack two reads in one frame. The other half of the same complaint is the read on the way back in: the link may have been asleep as long as the app was, and the first request across one still coming back times out at the five seconds every request gets. The resume's read alone retries once. It costs nothing when the server really is down, because a refused connection comes back at once rather than waiting out a timeout -- and it is a retry, not a guess. Checked all four ways on the emulator, including the two the change was not written for. Killing the server while the app was backgrounded mid-poll and returning: the list loads, no timeout. Pressing Refresh with the server down: the failure is shown at once, in full. Returning while it is still down: retried, then reported, no stuck spinner. Returning once it is back: the list recovers, which the old guard would have prevented.
This commit is contained in:
1 parent
a7f7f4550e
commit
3685ab107d
2 files changed
+168
-46
No files matched your search
@@ -335,6 +335,41 @@ mutable at runtime from the phone.
|
||||
on *that* answer, which the card's own refresh control and a freshly
|
||||
added app both use. Waiting on the whole list to settle would make one
|
||||
card's refresh sit behind another card's.
|
||||
Arriving and resuming are **one** event, not two: `LifecycleResumeEffect`
|
||||
runs when the screen first reaches RESUMED, so there is no
|
||||
`LaunchedEffect(Unit)` beside it and one thing decides when the list is
|
||||
read. It refreshes from *any* state including a failed one -- guarded on
|
||||
"loaded", as it used to be, a card that had gone red stayed red until
|
||||
somebody found the Refresh button, which is the opposite of what
|
||||
returning to an app should do. `loadingList` is what stops the first
|
||||
composition and the first resume stacking two reads, and it is claimed
|
||||
*before* the coroutine launches, because both run in the same frame and
|
||||
a flag set inside the coroutine is set too late to be a guard.
|
||||
|
||||
- **A failure that lands while nobody is looking is not shown.** Work
|
||||
started before the app went away keeps running -- deliberately, since a
|
||||
download that finishes in the background is a download that worked -- and
|
||||
when the device sleeps or the link drops it fails. Reported, that meant
|
||||
coming back an hour later to a five-second read timeout that said nothing
|
||||
about the server and that there was nothing left to do about. So every
|
||||
catch in `UpdaterScreen.kt` goes through `failure(e)`, which answers null
|
||||
while the screen is not resumed, and **null means clear, never leave** at
|
||||
every site -- a dropped failure that left the card alone would leave a
|
||||
spinner up for an operation that has already stopped. `setProject` and
|
||||
`setComponent` both take a nullable state so the answer can be handed
|
||||
straight over. Iris asked for this on 2026-09-01: "I got a socket timeout
|
||||
by leaving the app for too long. Make sure not to show that if the app
|
||||
just gets unloaded."
|
||||
The other half of the same complaint is the read on the way back *in*:
|
||||
the link may have been asleep as long as the app was, and the first
|
||||
request across one still coming back times out at the five seconds every
|
||||
request gets. So the resume's read alone retries once (`afterAGap`),
|
||||
which costs nothing when the server really is down -- a refused
|
||||
connection comes back at once rather than waiting out a timeout. That is
|
||||
a retry, not a guess: nothing is displayed that was not measured.
|
||||
Dropping a failure is only safe *because* the resume re-reads from any
|
||||
state; the two changes hold each other up, and undoing either alone
|
||||
leaves the list stuck on a spinner or stuck on a stale error.
|
||||
|
||||
- **The Add screen is drawn over the list, not in place of it.** Swapped
|
||||
out, the list is composed again from nothing on the way back -- and
|
||||
|
||||
Reference in new issue
Block a user