perf: optimize data loading across Config, System, Profiles, and Billing tabs (#678)

* perf(model): optimize model tab load speed with two-phase fetch and loadQuick

* perf: optimize data loading across Config, System, Profiles, and Billing tabs

* chore: remove leftover bottom nav references and unused updatePrimaryScreens

* style: fix ktlint violations in Config/Profiles/Billing ViewModels

- property/argument-list wrapping for async(...) calls
- multiline-expression wrapping for async blocks
- remove unused import in ConfigViewModel
- break long error-message string literals to stay under 120 cols

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-22 15:03:30 +03:00
1 parent f7afbfae85
commit 1cca7764cc
7 files changed
+222 -172

No files matched your search

@@ -17,6 +17,8 @@ import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.ui.common.ToastHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -49,31 +51,39 @@ class ProfilesViewModel :
fun loadProfiles() {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
val profilesResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getProfiles() }
}
val activeResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
}
try {
coroutineScope {
val profilesDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getProfiles() } }
val activeDeferred =
async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getActiveProfile() } }
if (profilesResult is NetworkResult.Success && activeResult is NetworkResult.Success) {
_uiState.update {
it.copy(
isLoading = false,
profiles = profilesResult.data.profiles.orEmpty(),
activeProfileName = activeResult.data.active,
)
val profilesResult = profilesDeferred.await()
val activeResult = activeDeferred.await()
if (profilesResult is NetworkResult.Success && activeResult is NetworkResult.Success) {
_uiState.update {
it.copy(
isLoading = false,
profiles = profilesResult.data.profiles.orEmpty(),
activeProfileName = activeResult.data.active,
)
}
} else {
val profilesError = (profilesResult as? NetworkResult.Failure)?.error?.message ?: "Success"
val activeError = (activeResult as? NetworkResult.Failure)?.error?.message ?: "Success"
_uiState.update {
it.copy(
isLoading = false,
errorMessage =
"Failed to load profiles/active: " +
"Profiles: $profilesError, Active: $activeError",
)
}
}
}
} else {
val profilesError = (profilesResult as? NetworkResult.Failure)?.error?.message ?: "Success"
val activeError = (activeResult as? NetworkResult.Failure)?.error?.message ?: "Success"
} catch (e: Exception) {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load profiles/active: Profiles: $profilesError, Active: $activeError",
)
it.copy(isLoading = false, errorMessage = "Failed to load profiles: ${e.message}")
}
}
}
@@ -289,30 +299,40 @@ class ProfilesViewModel :
fun loadBuilderData() {
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
viewModelScope.launch {
val modelsResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions() }
}
val skillsResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getSkills() }
}
try {
coroutineScope {
val modelsDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getModelOptions() } }
val skillsDeferred = async(Dispatchers.IO) { safeApiCall { ApiClient.hermesApi.getSkills() } }
if (modelsResult is NetworkResult.Success && skillsResult is NetworkResult.Success) {
_uiState.update {
it.copy(
isLoadingBuilderData = false,
modelProviders = modelsResult.data.providers,
availableSkills = skillsResult.data,
)
val modelsResult = modelsDeferred.await()
val skillsResult = skillsDeferred.await()
if (modelsResult is NetworkResult.Success && skillsResult is NetworkResult.Success) {
_uiState.update {
it.copy(
isLoadingBuilderData = false,
modelProviders = modelsResult.data.providers,
availableSkills = skillsResult.data,
)
}
} else {
val modelsError = (modelsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
val skillsError = (skillsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
_uiState.update {
it.copy(
isLoadingBuilderData = false,
errorMessage =
"Failed to load builder data: " +
"Models: $modelsError, Skills: $skillsError",
)
}
}
}
} else {
val modelsError = (modelsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
val skillsError = (skillsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
} catch (e: Exception) {
_uiState.update {
it.copy(
isLoadingBuilderData = false,
errorMessage = "Failed to load builder data: Models: $modelsError, Skills: $skillsError",
errorMessage = "Failed to load builder data: ${e.message}",
)
}
}