fix(ui): keep previous search results while reloading
Keep showing the last results while a new search generation loads, and guard list access: the paging snapshot can shrink mid-layout, which threw and crashed on an empty result set.
This commit is contained in:
1 parent
447d086cdf
commit
b5fc7e7246
2 files changed
+37
-5
No files matched your search
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.ime
|
|||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -50,6 +51,7 @@ internal fun SearchResultsPane(
|
|||||||
var restoredScrollPosition by remember { mutableStateOf(false) }
|
var restoredScrollPosition by remember { mutableStateOf(false) }
|
||||||
var pendingScrollToTop by remember { mutableStateOf(false) }
|
var pendingScrollToTop by remember { mutableStateOf(false) }
|
||||||
var sawRefreshForPendingScroll by remember { mutableStateOf(false) }
|
var sawRefreshForPendingScroll by remember { mutableStateOf(false) }
|
||||||
|
var staleEntries by remember { mutableStateOf(emptyList<IndexEntry>()) }
|
||||||
val imeBottomPadding = with(LocalDensity.current) {
|
val imeBottomPadding = with(LocalDensity.current) {
|
||||||
WindowInsets.ime.getBottom(this).toDp()
|
WindowInsets.ime.getBottom(this).toDp()
|
||||||
}
|
}
|
||||||
@@ -72,8 +74,27 @@ internal fun SearchResultsPane(
|
|||||||
restoredScrollPosition = true
|
restoredScrollPosition = true
|
||||||
pendingScrollToTop = query.isNotBlank()
|
pendingScrollToTop = query.isNotBlank()
|
||||||
sawRefreshForPendingScroll = false
|
sawRefreshForPendingScroll = false
|
||||||
|
if (query.isBlank()) {
|
||||||
|
staleEntries = emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val loaded = ArrayList<IndexEntry>(results.itemCount)
|
||||||
|
for (index in 0 until results.itemCount) {
|
||||||
|
val entry = results.peek(index)
|
||||||
|
if (entry != null) {
|
||||||
|
loaded.add(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SideEffect {
|
||||||
|
if (results.loadState.refresh is LoadState.NotLoading && loaded.isNotEmpty()) {
|
||||||
|
staleEntries = loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val pendingReload = results.itemCount == 0 &&
|
||||||
|
results.loadState.refresh is LoadState.Loading
|
||||||
|
val shownStale = if (pendingReload && searchState.text.isNotBlank()) staleEntries else emptyList()
|
||||||
|
|
||||||
LaunchedEffect(results.loadState.refresh, pendingScrollToTop) {
|
LaunchedEffect(results.loadState.refresh, pendingScrollToTop) {
|
||||||
if (pendingScrollToTop && results.loadState.refresh is LoadState.Loading) {
|
if (pendingScrollToTop && results.loadState.refresh is LoadState.Loading) {
|
||||||
@@ -125,6 +146,7 @@ internal fun SearchResultsPane(
|
|||||||
listState = listState,
|
listState = listState,
|
||||||
showDictionaryName = showDictionaryName,
|
showDictionaryName = showDictionaryName,
|
||||||
contentPadding = PaddingValues(bottom = imeBottomPadding),
|
contentPadding = PaddingValues(bottom = imeBottomPadding),
|
||||||
|
staleEntries = shownStale,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,11 @@ fun SearchResultsList(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
listState: LazyListState = rememberLazyListState(),
|
listState: LazyListState = rememberLazyListState(),
|
||||||
showDictionaryName: Boolean = true,
|
showDictionaryName: Boolean = true,
|
||||||
contentPadding: PaddingValues = PaddingValues()
|
contentPadding: PaddingValues = PaddingValues(),
|
||||||
|
staleEntries: List<IndexEntry> = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
val showStale = staleEntries.isNotEmpty() && results.itemCount == 0
|
||||||
|
val rowCount = if (showStale) staleEntries.size else results.itemCount
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.testTag("search_results")
|
.testTag("search_results")
|
||||||
@@ -41,11 +44,18 @@ fun SearchResultsList(
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
count = results.itemCount,
|
count = rowCount,
|
||||||
key = results.itemKey { "${it.dictionaryPath}:${it.offset.value}:${it.word}" },
|
key = { index ->
|
||||||
contentType = results.itemContentType { "search_result" }
|
val entry = if (showStale) staleEntries.getOrNull(index)
|
||||||
|
else if (index in 0 until results.itemCount) results.peek(index) else null
|
||||||
|
entry?.let { "${it.dictionaryPath}:${it.offset.value}:${it.word}" }
|
||||||
|
?: "search_result_$index"
|
||||||
|
},
|
||||||
|
contentType = { "search_result" }
|
||||||
) { index ->
|
) { index ->
|
||||||
results[index]?.let { entry ->
|
val entry = if (showStale) staleEntries.getOrNull(index)
|
||||||
|
else if (index in 0 until results.itemCount) results.peek(index) else null
|
||||||
|
if (entry != null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.animateItem(
|
modifier = Modifier.animateItem(
|
||||||
fadeInSpec = if (index < 8) tween(150) else null,
|
fadeInSpec = if (index < 8) tween(150) else null,
|
||||||
|
|||||||
Reference in new issue
Block a user