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
@@ -45,6 +45,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -57,6 +58,7 @@ import androidx.core.content.FileProvider
|
||||
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.data.model.ManagedFileEntry
|
||||
import com.m57.hermescontrol.ui.common.EmptyState
|
||||
import com.m57.hermescontrol.ui.common.ErrorState
|
||||
@@ -79,6 +81,7 @@ fun FilesScreen(
|
||||
viewModel: FilesViewModel = viewModel { FilesViewModel() },
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
val uploadLauncher =
|
||||
@@ -104,6 +107,13 @@ fun FilesScreen(
|
||||
onDispose { viewModel.clearTransientState() }
|
||||
}
|
||||
|
||||
// Scope-aware load: a new server/profile context must never keep the old
|
||||
// server's directory path or entries on screen (PR #1192 cache scoping).
|
||||
LaunchedEffect(dataScope) {
|
||||
viewModel.clearScopeOwnedState()
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
val hasEntries = state.entries.isNotEmpty()
|
||||
|
||||
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||
|
||||
@@ -3,15 +3,18 @@ package com.m57.hermescontrol.ui.files
|
||||
import android.util.Base64
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.local.SwrCache
|
||||
import com.m57.hermescontrol.data.model.ManagedDirectoryCreate
|
||||
import com.m57.hermescontrol.data.model.ManagedFileActionResponse
|
||||
import com.m57.hermescontrol.data.model.ManagedFileDelete
|
||||
import com.m57.hermescontrol.data.model.ManagedFileEntry
|
||||
import com.m57.hermescontrol.data.model.ManagedFilesListResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
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.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -76,8 +79,52 @@ class FilesViewModel :
|
||||
private val _uiState = MutableStateFlow(FilesUiState())
|
||||
val uiState: StateFlow<FilesUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun load(path: String? = null) {
|
||||
safeLaunchLoad(
|
||||
private val filesCache = SwrCache<String, ManagedFilesListResponse>()
|
||||
|
||||
/**
|
||||
* Reset state owned by the previous server/profile context (issue: PR #1192 cache scoping).
|
||||
* The current directory path belongs to the old server and must not leak into the new one.
|
||||
*/
|
||||
fun clearScopeOwnedState() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
currentPath = "",
|
||||
parentPath = null,
|
||||
crumbs = emptyList(),
|
||||
entries = emptyList(),
|
||||
isLoading = false,
|
||||
errorMessage = null,
|
||||
busyPaths = emptySet(),
|
||||
isCreatingDir = false,
|
||||
newDirName = "",
|
||||
isUploading = false,
|
||||
deleteTarget = null,
|
||||
openingPath = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(
|
||||
path: String? = null,
|
||||
forceRefresh: Boolean = false,
|
||||
) {
|
||||
val cacheKey = path ?: ""
|
||||
safeLaunchSwrLoad(
|
||||
cache = filesCache,
|
||||
cacheKey = cacheKey,
|
||||
forceRefresh = forceRefresh,
|
||||
onCacheHit = { data ->
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
currentPath = data.path,
|
||||
parentPath = data.parent,
|
||||
crumbs = buildCrumbs(data.path),
|
||||
entries = data.entries,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
apiCall = {
|
||||
safeApiCall { ApiClient.hermesApi.listManagedFiles(path) }
|
||||
},
|
||||
@@ -104,7 +151,7 @@ class FilesViewModel :
|
||||
)
|
||||
}
|
||||
|
||||
fun refresh() = load(_uiState.value.currentPath.ifBlank { null })
|
||||
fun refresh() = load(_uiState.value.currentPath.ifBlank { null }, forceRefresh = true)
|
||||
|
||||
fun navigateTo(entry: ManagedFileEntry) {
|
||||
if (!entry.isDirectory) return
|
||||
|
||||
Reference in new issue
Block a user