Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 97fe6947 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

feat: enforce fresh paging per submit to avoid stale results on repeat queries

parent 75f02d23
Loading
Loading
Loading
Loading
Loading
+72 −16
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow
@@ -322,13 +324,27 @@ private fun PagingPlayStoreResultList(
    }

    val loadState = lazyItems.loadState
    val errorState = loadState.refresh as? LoadState.Error
        ?: loadState.prepend as? LoadState.Error
        ?: loadState.append as? LoadState.Error
    val isRefreshing = loadState.refresh is LoadState.Loading
    val refreshState = loadState.refresh
    val refreshError = refreshState as? LoadState.Error
    val appendError = loadState.append as? LoadState.Error
    val prependError = loadState.prepend as? LoadState.Error
    val isRefreshing = refreshState is LoadState.Loading
    val isAppending = loadState.append is LoadState.Loading
    val isError = errorState != null
    val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0

    val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) }

    LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) {
        if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) {
            hasLoadedCurrentQuery.value = true
        }
        if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) {
            hasLoadedCurrentQuery.value = false
        }
    }

    val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value
    val showFooterError = hasLoadedCurrentQuery.value && listOf(refreshError, appendError, prependError).any { it != null }
    val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0

    Box(modifier = modifier) {
        when {
@@ -336,10 +352,11 @@ private fun PagingPlayStoreResultList(
                SearchShimmerList()
            }

            isError -> {
            initialLoadError -> {
                SearchErrorState(
                    onRetry = { lazyItems.retry() },
                    modifier = Modifier.fillMaxSize()
                    modifier = Modifier.fillMaxSize(),
                    fullScreen = true,
                )
            }

@@ -399,6 +416,18 @@ private fun PagingPlayStoreResultList(
                            }
                        }
                    }

                    if (showFooterError) {
                        item(key = "error_footer_play_store") {
                            SearchErrorState(
                                onRetry = { lazyItems.retry() },
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .padding(horizontal = 16.dp, vertical = 12.dp),
                                fullScreen = false,
                            )
                        }
                    }
                }
            }
        }
@@ -438,13 +467,27 @@ private fun PagingSearchResultList(
    }

    val loadState = lazyItems.loadState
    val errorState = loadState.refresh as? LoadState.Error
        ?: loadState.prepend as? LoadState.Error
        ?: loadState.append as? LoadState.Error
    val isRefreshing = loadState.refresh is LoadState.Loading
    val refreshState = loadState.refresh
    val refreshError = refreshState as? LoadState.Error
    val appendError = loadState.append as? LoadState.Error
    val prependError = loadState.prepend as? LoadState.Error
    val isRefreshing = refreshState is LoadState.Loading
    val isAppending = loadState.append is LoadState.Loading
    val isError = errorState != null
    val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0

    val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) }

    LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) {
        if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) {
            hasLoadedCurrentQuery.value = true
        }
        if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) {
            hasLoadedCurrentQuery.value = false
        }
    }

    val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value
    val showFooterError = hasLoadedCurrentQuery.value && listOf(refreshError, appendError, prependError).any { it != null }
    val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0

    Box(modifier = modifier) {
        when {
@@ -452,10 +495,11 @@ private fun PagingSearchResultList(
                SearchShimmerList()
            }

            isError -> {
            initialLoadError -> {
                SearchErrorState(
                    onRetry = { lazyItems.retry() },
                    modifier = Modifier.fillMaxSize()
                    modifier = Modifier.fillMaxSize(),
                    fullScreen = true,
                )
            }

@@ -516,6 +560,18 @@ private fun PagingSearchResultList(
                            }
                        }
                    }

                    if (showFooterError) {
                        item(key = "error_footer") {
                            SearchErrorState(
                                onRetry = { lazyItems.retry() },
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .padding(horizontal = 16.dp, vertical = 12.dp),
                                fullScreen = false,
                            )
                        }
                    }
                }
            }
        }
+34 −2
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ package foundation.e.apps.ui.compose.components.search
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -13,22 +14,37 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import foundation.e.apps.R
import foundation.e.apps.ui.compose.theme.AppTheme

@Composable
fun SearchErrorState(
    onRetry: () -> Unit,
    modifier: Modifier = Modifier,
    fullScreen: Boolean = true,
) {
    val containerModifier = if (fullScreen) {
        modifier.fillMaxSize()
    } else {
        modifier.fillMaxWidth()
    }

    val contentPadding = if (fullScreen) {
        PaddingValues(all = 24.dp)
    } else {
        PaddingValues(horizontal = 16.dp, vertical = 12.dp)
    }

    Box(
        modifier = modifier.fillMaxSize(),
        modifier = containerModifier,
        contentAlignment = Alignment.Center
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(24.dp),
                .padding(contentPadding),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(12.dp),
        ) {
@@ -43,3 +59,19 @@ fun SearchErrorState(
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun SearchErrorStateFullScreenPreview() {
    AppTheme {
        SearchErrorState(onRetry = {}, fullScreen = true)
    }
}

@Preview(showBackground = true)
@Composable
private fun SearchErrorStateFooterPreview() {
    AppTheme {
        SearchErrorState(onRetry = {}, fullScreen = false)
    }
}
+14 −7
Original line number Diff line number Diff line
@@ -110,6 +110,7 @@ class SearchViewModelV2 @Inject constructor(
    private data class SearchRequest(
        val query: String,
        val visibleTabs: List<SearchTabType>,
        val version: Int,
    )

    private val searchRequests = MutableStateFlow<SearchRequest?>(null)
@@ -254,7 +255,9 @@ class SearchViewModelV2 @Inject constructor(
        val selectedTab = _uiState.value.selectedTab?.takeIf { visibleTabs.contains(it) }
            ?: visibleTabs.firstOrNull()

        var nextVersion = _uiState.value.searchVersion + 1
        _uiState.update { current ->
            nextVersion = current.searchVersion + 1
            current.copy(
                query = trimmedQuery,
                suggestions = emptyList(),
@@ -262,14 +265,15 @@ class SearchViewModelV2 @Inject constructor(
                availableTabs = visibleTabs,
                selectedTab = selectedTab,
                hasSubmittedSearch = visibleTabs.isNotEmpty(),
                searchVersion = current.searchVersion + 1,
                searchVersion = nextVersion,
            )
        }

        if (visibleTabs.isNotEmpty()) {
            searchRequests.value = SearchRequest(
                query = trimmedQuery,
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = nextVersion,
            )
            _scrollPositions.update { emptyMap() }
        }
@@ -299,17 +303,20 @@ class SearchViewModelV2 @Inject constructor(
            )
        }

        val currentQuery = _uiState.value.query
        val shouldUpdateRequest = _uiState.value.hasSubmittedSearch && currentQuery.isNotBlank()
        val currentState = _uiState.value
        val currentQuery = currentState.query
        val shouldUpdateRequest = currentState.hasSubmittedSearch && currentQuery.isNotBlank()
        if (shouldUpdateRequest && visibleTabs.isNotEmpty()) {
            searchRequests.value = SearchRequest(
                query = currentQuery,
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = currentState.searchVersion,
            )
        } else if (!_uiState.value.hasSubmittedSearch) {
        } else if (!currentState.hasSubmittedSearch) {
            searchRequests.value = SearchRequest(
                query = "",
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = currentState.searchVersion,
            )
        }
    }