Keep the browsed folder and view settings across process death
Wires SavedStateHandle into BrowserViewModel: the folder being browsed plus the sort key, direction, view mode and hidden-files setting are restored on relaunch, so a process death no longer drops the user back at the top of the tree. Deliberately excluded, and documented as such: the search query (restoring one would hide the folder contents on launch with no visible cause), the selection, and the clipboard (a paste that survived a reboot would be a nasty surprise). Four tests cover the restored state and each exclusion.
This commit is contained in:
@@ -2,8 +2,10 @@ package de.softwareapp_hb.filemanager.ui.browser
|
||||
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
@@ -47,14 +49,19 @@ import kotlinx.coroutines.launch
|
||||
* [BrowserUiState.conflict] for the user to answer), and it never shows raw storage paths
|
||||
* as labels — the root list is built from `VolumeInfo` so "Internal storage" can be
|
||||
* localised.
|
||||
*
|
||||
* [savedState] carries the folder being browsed plus the sort, view mode and hidden-files
|
||||
* settings across process death; see [restoredViewState] for what is deliberately left
|
||||
* out.
|
||||
*/
|
||||
class BrowserViewModel(
|
||||
private val storage: StorageLocation,
|
||||
private val notifier: OperationNotifier = OperationNotifier.None,
|
||||
private val deleteRequests: DeleteRequestFactory = DeleteRequestFactory { _, _ -> null },
|
||||
private val savedState: SavedStateHandle = SavedStateHandle(),
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(BrowserUiState())
|
||||
private val _state = MutableStateFlow(restoredViewState())
|
||||
val state: StateFlow<BrowserUiState> = _state.asStateFlow()
|
||||
|
||||
/** The unfiltered listing of the current directory; [recompute] derives what is shown. */
|
||||
@@ -115,6 +122,7 @@ class BrowserViewModel(
|
||||
)
|
||||
}
|
||||
recompute()
|
||||
persistViewState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +142,7 @@ class BrowserViewModel(
|
||||
)
|
||||
}
|
||||
recompute()
|
||||
persistViewState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +191,51 @@ class BrowserViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------- persisted view state
|
||||
|
||||
/**
|
||||
* The view state that outlives the process: where the user was, and how they were
|
||||
* looking at it.
|
||||
*
|
||||
* Deliberately narrow. A folder is worth coming back to — losing your place deep in a
|
||||
* tree is the most annoying thing an Android file manager can do — and a sort order or
|
||||
* view mode that resets on every process death is just noise. Three things are left
|
||||
* out on purpose: the **search query**, because restoring one would hide the folder's
|
||||
* contents on launch with no visible cause; the **selection**, which is transient; and
|
||||
* the **clipboard**, because a paste that survived a reboot would be a nasty surprise.
|
||||
*/
|
||||
private fun restoredViewState(): BrowserUiState {
|
||||
val savedSortKey = savedState.get<String>(KEY_SORT_KEY)
|
||||
val savedDirection = savedState.get<String>(KEY_SORT_DIRECTION)
|
||||
val savedViewMode = savedState.get<String>(KEY_VIEW_MODE)
|
||||
return BrowserUiState(
|
||||
path = savedState.get<String>(KEY_PATH)?.let { StoragePath.of(it) },
|
||||
sort = SortSpec(
|
||||
key = SortKey.entries.firstOrNull { it.name == savedSortKey } ?: SortKey.NAME,
|
||||
direction = SortDirection.entries.firstOrNull { it.name == savedDirection }
|
||||
?: SortDirection.ASCENDING,
|
||||
),
|
||||
viewMode = ViewMode.entries.firstOrNull { it.name == savedViewMode } ?: ViewMode.LIST,
|
||||
showHidden = savedState.get<Boolean>(KEY_SHOW_HIDDEN) ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot the restorable view state.
|
||||
*
|
||||
* Called wherever one of those fields changes rather than collected from the state
|
||||
* flow, so what is persisted stays an explicit, greppable list rather than "whatever
|
||||
* happens to be in the state".
|
||||
*/
|
||||
private fun persistViewState() {
|
||||
val current = _state.value
|
||||
savedState[KEY_PATH] = current.path?.value
|
||||
savedState[KEY_SORT_KEY] = current.sort.key.name
|
||||
savedState[KEY_SORT_DIRECTION] = current.sort.direction.name
|
||||
savedState[KEY_VIEW_MODE] = current.viewMode.name
|
||||
savedState[KEY_SHOW_HIDDEN] = current.showHidden
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- view preferences
|
||||
|
||||
fun setQuery(query: String) {
|
||||
@@ -192,6 +246,7 @@ class BrowserViewModel(
|
||||
fun setSort(key: SortKey) {
|
||||
_state.update { it.copy(sort = it.sort.copy(key = key)) }
|
||||
recompute()
|
||||
persistViewState()
|
||||
}
|
||||
|
||||
fun toggleSortDirection() {
|
||||
@@ -204,15 +259,18 @@ class BrowserViewModel(
|
||||
it.copy(sort = it.sort.copy(direction = flipped))
|
||||
}
|
||||
recompute()
|
||||
persistViewState()
|
||||
}
|
||||
|
||||
fun toggleShowHidden() {
|
||||
_state.update { it.copy(showHidden = !it.showHidden) }
|
||||
recompute()
|
||||
persistViewState()
|
||||
}
|
||||
|
||||
fun setViewMode(viewMode: ViewMode) {
|
||||
_state.update { it.copy(viewMode = viewMode) }
|
||||
persistViewState()
|
||||
}
|
||||
|
||||
/** Called after the system folder picker returns, with whether the grant was kept. */
|
||||
@@ -600,6 +658,12 @@ class BrowserViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PATH = "browser.path"
|
||||
private const val KEY_SORT_KEY = "browser.sortKey"
|
||||
private const val KEY_SORT_DIRECTION = "browser.sortDirection"
|
||||
private const val KEY_VIEW_MODE = "browser.viewMode"
|
||||
private const val KEY_SHOW_HIDDEN = "browser.showHidden"
|
||||
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val app = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
|
||||
@@ -608,6 +672,7 @@ class BrowserViewModel(
|
||||
storage = app.storage,
|
||||
notifier = app.notifier,
|
||||
deleteRequests = app.deleteRequests,
|
||||
savedState = createSavedStateHandle(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user