diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt index b0cdab9..63cee19 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt @@ -55,9 +55,10 @@ fun SpawnScreen( onBack: () -> Unit, ) { val scope = rememberCoroutineScope() - var providers by remember { mutableStateOf>(emptyList()) } - var hosts by remember { mutableStateOf>(emptyList()) } - var loading by remember { mutableStateOf(true) } + // What the form is made of, and whether we have it yet. A failure here + // is not the same as a server with nothing to offer, so it must not + // reach the pickers as empty lists -- see LoadState. + var options by remember { mutableStateOf>(LoadState.Loading) } var provider by remember { mutableStateOf(null) } var host by remember { mutableStateOf(null) } @@ -66,20 +67,21 @@ fun SpawnScreen( var cwd by remember { mutableStateOf("") } var permissionMode by remember { mutableStateOf("manual") } var busy by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(null) } + // Only the spawn's own failure. The fetch's lives in `options`: this + // one leaves a filled-in form worth keeping, and that one leaves + // nothing to fill in. + var spawnError by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - try { - val loaded = withContext(Dispatchers.IO) { - fetchProviders(settings) to fetchHosts(settings) + options = try { + val fetched = withContext(Dispatchers.IO) { + SpawnOptions(fetchProviders(settings), fetchHosts(settings)) } - providers = loaded.first - hosts = loaded.second - provider = providers.firstOrNull() + provider = fetched.providers.firstOrNull() + LoadState.Loaded(fetched) } catch (e: ApiException) { - error = e.message + LoadState.failed(e) } - loading = false } val current = provider @@ -99,9 +101,20 @@ fun SpawnScreen( } Spacer(Modifier.height(16.dp)) - if (loading) { - CircularProgressIndicator() - return@Column + // Nothing below is fillable until the options are here, and a + // failure to fetch them leaves no form worth showing -- so this + // reports and stops, rather than offering empty pickers under an + // error message. + val (providers, hosts) = when (val state = options) { + is LoadState.Loading -> { + CircularProgressIndicator() + return@Column + } + is LoadState.Error -> { + Text(state.message, color = MaterialTheme.colorScheme.error) + return@Column + } + is LoadState.Loaded -> state.value } ChipGroup( @@ -177,7 +190,8 @@ fun SpawnScreen( } Spacer(Modifier.height(24.dp)) - error?.let { + // Beside the button that produced it. + spawnError?.let { Text(it, color = MaterialTheme.colorScheme.error) Spacer(Modifier.height(8.dp)) } @@ -201,7 +215,7 @@ fun SpawnScreen( } onSpawned(spawned) } catch (e: ApiException) { - error = e.message + spawnError = e.message busy = false } } @@ -211,6 +225,9 @@ fun SpawnScreen( } } +/** What the spawn form is built from, fetched as one thing. */ +private data class SpawnOptions(val providers: List, val hosts: List) + /** * A labeled row of choices that wraps onto as many lines as it needs. *