perf: optimize screen load times with progressive un-gating and SWR caching (#1192)
This commit is contained in:
55 files changed
+2494
-401
No files matched your search
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
@@ -43,11 +44,13 @@ fun SkillsScreen(
|
||||
viewModel: SkillsViewModel = viewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
|
||||
var query by remember { mutableStateOf("") }
|
||||
var selectedStatus by remember { mutableStateOf(SkillFilter.ALL_STATUSES) }
|
||||
var selectedCategory by remember { mutableStateOf(CATEGORY_ALL) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(dataScope) {
|
||||
viewModel.clearScopeOwnedState()
|
||||
viewModel.loadSkills()
|
||||
}
|
||||
|
||||
@@ -58,7 +61,7 @@ fun SkillsScreen(
|
||||
isRefreshing = state.isLoading,
|
||||
onRefresh = {
|
||||
if (state.viewMode == SkillsViewMode.INSTALLED) {
|
||||
viewModel.loadSkills()
|
||||
viewModel.loadSkills(forceRefresh = true)
|
||||
} else {
|
||||
viewModel.loadHubSources()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.local.DataScope
|
||||
import com.m57.hermescontrol.data.local.SwrCache
|
||||
import com.m57.hermescontrol.data.model.HubSkill
|
||||
import com.m57.hermescontrol.data.model.SaveSkillContentRequest
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
@@ -17,6 +20,7 @@ import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -93,11 +97,35 @@ class SkillsViewModel(
|
||||
|
||||
private var loadJob: Job? = null
|
||||
private var searchJob: Job? = null
|
||||
private val skillsCache = SwrCache<String, List<Skill>>()
|
||||
|
||||
fun loadSkills() {
|
||||
fun clearScopeOwnedState() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
skills = emptyList(),
|
||||
errorMessage = null,
|
||||
editingSkillName = null,
|
||||
skillContent = null,
|
||||
isLoadingContent = false,
|
||||
isSavingContent = false,
|
||||
saveContentSuccess = false,
|
||||
previewSkillName = null,
|
||||
previewSkillContent = null,
|
||||
isLoadingPreview = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSkills(forceRefresh: Boolean = false) {
|
||||
loadJob =
|
||||
safeLaunchLoad(
|
||||
safeLaunchSwrLoad(
|
||||
cache = skillsCache,
|
||||
forceRefresh = forceRefresh,
|
||||
currentJob = loadJob,
|
||||
onCacheHit = { cached ->
|
||||
_uiState.update { it.copy(isLoading = false, skills = cached, errorMessage = null) }
|
||||
},
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getSkills() } },
|
||||
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||
onSuccess = { data ->
|
||||
@@ -328,7 +356,7 @@ class SkillsViewModel(
|
||||
)
|
||||
}
|
||||
// Refresh installed skills
|
||||
loadSkills()
|
||||
loadSkills(forceRefresh = true)
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
@@ -365,7 +393,7 @@ class SkillsViewModel(
|
||||
toastMessage = "Uninstalled: $name",
|
||||
)
|
||||
}
|
||||
loadSkills()
|
||||
loadSkills(forceRefresh = true)
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
@@ -396,7 +424,9 @@ class SkillsViewModel(
|
||||
fun toggleSkill(skill: Skill) {
|
||||
val originalEnabled = skill.enabled
|
||||
val targetEnabled = !originalEnabled
|
||||
val requestScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||
|
||||
// Optimistically update
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
skills =
|
||||
@@ -411,12 +441,23 @@ class SkillsViewModel(
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) }
|
||||
}
|
||||
if (result is NetworkResult.Failure) {
|
||||
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||
if (requestScope != null && currentScope != requestScope) return@launch
|
||||
if (result is NetworkResult.Success) {
|
||||
val updated = _uiState.value.skills
|
||||
if (requestScope != null) {
|
||||
skillsCache.put(requestScope.scopedKey("default"), updated)
|
||||
}
|
||||
} else if (result is NetworkResult.Failure) {
|
||||
revertSkillToggle(
|
||||
skill.name,
|
||||
originalEnabled,
|
||||
"Failed to toggle skill: ${result.error.message}",
|
||||
)
|
||||
val reverted = _uiState.value.skills
|
||||
if (requestScope != null) {
|
||||
skillsCache.put(requestScope.scopedKey("default"), reverted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,7 +566,7 @@ class SkillsViewModel(
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update { it.copy(toastMessage = "Skills updated") }
|
||||
loadSkills()
|
||||
loadSkills(forceRefresh = true)
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
|
||||
Reference in new issue
Block a user