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
@@ -48,6 +48,7 @@ import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
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.theme.LocalSpacing
|
||||
import com.m57.hermescontrol.ui.common.EmptyState
|
||||
import com.m57.hermescontrol.ui.common.ErrorState
|
||||
@@ -70,6 +71,7 @@ fun LogsScreen(
|
||||
viewModel: LogsViewModel = viewModel { LogsViewModel() },
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
|
||||
val spacing = LocalSpacing.current
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
@@ -115,7 +117,8 @@ fun LogsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(dataScope) {
|
||||
viewModel.clearScopeOwnedState()
|
||||
viewModel.loadLogs()
|
||||
}
|
||||
|
||||
@@ -146,7 +149,7 @@ fun LogsScreen(
|
||||
title = { Text(stringResource(R.string.screen_logs)) },
|
||||
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||
isRefreshing = state.isLoading,
|
||||
onRefresh = { viewModel.loadLogs() },
|
||||
onRefresh = { viewModel.loadLogs(forceRefresh = true) },
|
||||
actions = {
|
||||
// Pause / Resume toggle
|
||||
IconButton(onClick = { pauseScroll = !pauseScroll }) {
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.m57.hermescontrol.ui.logs
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.local.SwrCache
|
||||
import com.m57.hermescontrol.data.model.LogResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
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.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -40,43 +42,51 @@ class LogsViewModel :
|
||||
private val _uiState = MutableStateFlow(LogsUiState())
|
||||
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var loadInProgress = false
|
||||
|
||||
private var loadJob: Job? = null
|
||||
private var autoRefreshJob: Job? = null
|
||||
private val logsCache = SwrCache<String, LogResponse>()
|
||||
|
||||
fun loadLogs() {
|
||||
if (loadInProgress) return
|
||||
loadInProgress = true
|
||||
fun clearScopeOwnedState() {
|
||||
_uiState.update { it.copy(isLoading = false, logs = emptyList(), errorMessage = null) }
|
||||
}
|
||||
|
||||
fun loadLogs(forceRefresh: Boolean = false) {
|
||||
val filters = _uiState.value.filters
|
||||
safeLaunchLoad(
|
||||
apiCall = {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getLogs(
|
||||
file = filters.file,
|
||||
lines = filters.lines,
|
||||
level = filters.level,
|
||||
component = filters.component,
|
||||
)
|
||||
}
|
||||
},
|
||||
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||
onSuccess = { data ->
|
||||
val body = data
|
||||
val logsList = body.lines ?: body.logs ?: emptyList()
|
||||
_uiState.update { it.copy(isLoading = false, logs = logsList) }
|
||||
loadInProgress = false
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Failed to load logs: $errorMsg",
|
||||
)
|
||||
}
|
||||
loadInProgress = false
|
||||
},
|
||||
)
|
||||
val cacheKey = "${filters.file}:${filters.level}:${filters.component}:${filters.lines}"
|
||||
loadJob =
|
||||
safeLaunchSwrLoad(
|
||||
cache = logsCache,
|
||||
cacheKey = cacheKey,
|
||||
forceRefresh = forceRefresh,
|
||||
currentJob = loadJob,
|
||||
onCacheHit = { data ->
|
||||
val logsList = data.lines ?: data.logs ?: emptyList()
|
||||
_uiState.update { it.copy(isLoading = false, logs = logsList, errorMessage = null) }
|
||||
},
|
||||
apiCall = {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getLogs(
|
||||
file = filters.file,
|
||||
lines = filters.lines,
|
||||
level = filters.level,
|
||||
component = filters.component,
|
||||
)
|
||||
}
|
||||
},
|
||||
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||
onSuccess = { data ->
|
||||
val logsList = data.lines ?: data.logs ?: emptyList()
|
||||
_uiState.update { it.copy(isLoading = false, logs = logsList) }
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Failed to load logs: $errorMsg",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Update the server-side filters and immediately reload. */
|
||||
|
||||
Reference in new issue
Block a user