Ask again when the app comes back, so a failure cannot outstay it

Leaving the app and returning left "Couldn't reach the server" sitting at
the top of a list the server would by then answer perfectly well, and
nothing took it off until somebody pressed Refresh.

The four tabs draw a snapshot of a backend they are not connected to, so
what they show is only as fresh as the last answer. A stale *list* is a
small thing. A stale failure is not: it is a claim about right now, and
it is wrong in the direction that makes somebody go looking for a problem
that has already gone.

Returning to the foreground now bumps the same token the Refresh button
uses. One instruction the tabs already understand rather than a second
path into each of them -- which is also what makes this cover Import,
Models and Setups rather than only the list the report came from. Not on
first entry, since the tab composing already asks and bumping there would
make every cold start fetch twice.

Reproduced and fixed against the same sequence: server stopped, app
opened so the load fails, server started, app backgrounded and resumed
from the launcher. Before, the error is still there; after, the list is
drawn and current.
This commit is contained in:
iris committed 2026-08-29 23:48:15 -04:00
1 parent 2dc61c5780
commit 2264723ee3
1 file changed
+28
@@ -12,6 +12,7 @@ import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -20,6 +21,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
/** /**
* The app's root: one title, and four views of the backend behind it. * The app's root: one title, and four views of the backend behind it.
@@ -51,6 +55,30 @@ fun MainScreen(
var tab by remember { mutableStateOf(MainTab.Sessions) } var tab by remember { mutableStateOf(MainTab.Sessions) }
var refreshToken by remember { mutableIntStateOf(0) } var refreshToken by remember { mutableIntStateOf(0) }
// Coming back to the app asks again, on whichever tab is showing.
//
// What these four draw is a snapshot of a backend they are not connected to, so it is only as
// fresh as the last answer -- and a *failed* answer is the one that outstays its welcome. A
// phone that was away while the tunnel was down, or that fetched before the network came up,
// came back to "Couldn't reach the server" sitting at the top of a list the server would now
// answer for perfectly well, and nothing took it off until somebody pressed Refresh. A stale
// failure is worse than a stale list: it is a claim about right now.
//
// Through the same token the Refresh button uses, so this is one instruction the tabs already
// understand rather than a second path into each of them -- which is also what makes it cover
// all four rather than the one the report came from.
//
// Not on the first entry: the tab composing already asks, and bumping here would make every
// cold start fetch twice.
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner) {
var opening = true
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
if (!opening) refreshToken++
opening = false
}
}
// A tab the app put over the list has to step back to it rather than fall through to the // A tab the app put over the list has to step back to it rather than fall through to the
// system default, which closes the app -- that reads as a crash to somebody who only meant to // system default, which closes the app -- that reads as a crash to somebody who only meant to
// get back to their sessions. Nested inside AppRoot's handler, so it wins while it is enabled. // get back to their sessions. Nested inside AppRoot's handler, so it wins while it is enabled.