Home > OS >  Android Paging Compose: How to scroll to top when any of the queries changed?
Android Paging Compose: How to scroll to top when any of the queries changed?

Time:07-27

I have a list of products. On the ProductsScreen I have some filter options, search box, also filter chips :

I'm using paging 3 library for the pagination. I want to scroll to the top when the search query or selected filter chip has changed. Here is my code :

ProductScreen

@Composable
fun ProductsScreen(
    modifier: Modifier = Modifier,
    navController: NavController,
    viewModel: ProductsScreenViewModel = hiltViewModel(),
    onProductItemClick: (String) -> Unit = {},
    onProductFilterButtonClick: (ProductsFilterPreferences) -> Unit = {},
) {
    //products
    val products = viewModel.products.collectAsLazyPagingItems()
    
    //load state
    val isLoading = products.loadState.refresh is LoadState.Loading

    val scrollToTop = viewModel.scrollToTop.observeAsState()

    val scaffoldState = rememberScaffoldState()

    val listState = rememberLazyListState()

    val scope = rememberCoroutineScope()

    val query by viewModel.query.collectAsState()

    val context = LocalContext.current

    //not working
    LaunchedEffect(key1 = Unit) {
        scrollToTop.value?.getContentIfNotHandled()?.let { scroll ->
            if (scroll) {
                listState.scrollToItem(0)
            }
        }
    }

RubiBrandsScaffold(
        modifier = modifier,
        scaffoldState = scaffoldState,
        topBar = {
            ProductsScreenToolbar(
                showFilterBadge = viewModel.showFilterBadge,
                onFilterButtonClick = { onProductFilterButtonClick(viewModel.productsFilterPreferences.value) },
                searchQuery = query,
                onTrailingIconClick = {
                    viewModel.setQuery("")
                },
                onQueryChange = { query ->
                    viewModel.setQuery(query)
                },
                onSearchButtonClick = { query ->
                    viewModel.onSearch(query)
                },
                onChipClick = { chip ->
                    viewModel.onChipSelected(chip)
                },
                selectedChip = viewModel.selectedChip,
            )
        },
        floatingActionButton = {
            AnimatedVisibility(
                visible = showFab,
                enter = scaleIn(),
                exit = scaleOut()
            ) {
                FloatingActionButton(
                    backgroundColor = MaterialTheme.colors.primary,
                    onClick = {
                        scope.launch {
                            //working
                            listState.scrollToItem(0)
                        }
                    },
                    modifier = Modifier
                        .padding(
                            dimensionResource(id = R.dimen.dimen_16)
                        )
                ) {
                    Icon(
                        painter = painterResource(id = R.drawable.ic_up),
                        contentDescription = null,
                        tint = MaterialTheme.colors.onPrimary
                    )
                }
            }
        }
    ) { paddingValues ->
        SwipeRefresh(
            indicator = { state, trigger ->
                RubiBrandsSwipeRefreshIndicator(state = state, trigger = trigger)
            }, state = swipeRefreshState, onRefresh = products::refresh
        ) {
            LazyColumn(
                state = listState,
                modifier = Modifier.fillMaxSize()
            ) {
                items(products) { product ->
                    product?.let {
                        ProductItem(
                            modifier = Modifier.clickable {
                                if (!isLoading) onProductItemClick(
                                    product.id
                                )
                            },
                            childModifier = Modifier.shimmerModifier(isLoading),
                            productImage = product.productImage?.get(0),
                            productTitle = product.productTitle,
                            productCount = product.productCount,
                            productStatus = product.productStatus?.asString(context)
                        )
                    }
                }
                products.apply {
                    when {
                        loadState.source.refresh is LoadState.Loading -> {
                            items(10) {
                                ProductItem(
                                    childModifier = Modifier.shimmerModifier(true),
                                    productImage = null,
                                    productTitle = "",
                                    productCount = "",
                                    productStatus = ""
                                )
                            }
                        }

                        loadState.source.append is LoadState.Loading -> {
                            item {
                                CircularProgressIndicator(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .padding(16.dp)
                                        .wrapContentWidth(Alignment.CenterHorizontally)
                                )
                            }
                        }

                        loadState.source.refresh is LoadState.Error -> {
                            val e = products.loadState.refresh as LoadState.Error
                            item {
                                ErrorItem(
                                    modifier = Modifier.fillParentMaxSize(),
                                    message = e.error.localizedMessage
                                        ?: stringResource(id = R.string.something_went_wrong),
                                    onRetryClick = products::retry
                                )
                            }
                        }

                        loadState.source.append is LoadState.Error -> {
                            val e = products.loadState.append as LoadState.Error
                            item {
                                ErrorItem(
                                    modifier = Modifier
                                        .fillParentMaxWidth()
                                        .wrapContentHeight(),
                                    message = e.error.localizedMessage
                                        ?: stringResource(id = R.string.something_went_wrong),
                                    onRetryClick = products::retry
                                )
                            }
                        }

                        loadState.source.refresh is LoadState.NotLoading && loadState.source.refresh !is LoadState.Error && products.itemCount < 1 -> {
                            item {
                                RubiBrandsEmptyListView(modifier = Modifier.fillParentMaxSize())
                            }
                        }
                    }
                }
            }
        }
    }

}

Here is also my ProductsScreenViewModel :


@HiltViewModel
class ProductsScreenViewModel @Inject constructor(
    private val productsScreenUseCase: ProductsScreenUseCase
) : ViewModel() {


    var showFilterBadge by mutableStateOf(false)
        private set

    private val _query = MutableStateFlow<String>("")

    val query: StateFlow<String> = _query

    var selectedChip by mutableStateOf<ProductsChips>(ProductsChips.ALL)

    private val _productsFilterPreferences = MutableStateFlow(ProductsFilterPreferences())

    val productsFilterPreferences: StateFlow<ProductsFilterPreferences> = _productsFilterPreferences

    private val _scrollToTop = MutableLiveData<Event<Boolean>>()

    val scrollToTop: LiveData<Event<Boolean>> get() = _scrollToTop

    val products =
        _productsFilterPreferences.flatMapLatest { filterPreferences ->
            showFilterBadge = filterPreferences.sort.value != null
                    || filterPreferences.saleStatus.value != null
                    || filterPreferences.stockStatus.value != null
                    || filterPreferences.sku != null
                    || filterPreferences.priceOptions.value != R.string.all

            productsScreenUseCase.fetchProducts(params = filterPreferences)
        }.cachedIn(viewModelScope)


    fun setFilters(
        filters: ProductsFilterPreferences
    ) {
        _scrollToTop.value = Event(true)
        _productsFilterPreferences.update {
            it.copy(
                sort = filters.sort,
                state = filters.state,
                saleStatus = filters.saleStatus,
                stockStatus = filters.stockStatus,
                sku = filters.sku,
                priceOptions = filters.priceOptions
            )
        }
    }

    fun setQuery(query: String) {
        if (query.isEmpty()) {
            onSearch(query)
        }
        _query.value = query
    }

    fun onSearch(query: String) {
        //if I press the search button I'm setting scroll to top to true
        _scrollToTop.value = Event(true)
        _productsFilterPreferences.update {
            it.copy(query = query.trim().ifEmpty { null })
        }
    }

    fun onChipSelected(chip: ProductsChips) {
        selectedChip = chip
        //if I change the chip I'm setting scroll to top to true
        _scrollToTop.value = Event(true)
        _productsFilterPreferences.update {
            it.copy(state = chip.value)
        }
    }
}


There is a related question about that but it is valid for XML.

So whenever the search query or selected chip has changed I'm setting the scrollToTop value to true and then observing it from my ProductsScreen composable. But it is not working.

CodePudding user response:

You can try using the LaunchedEffect and the scrollState of Lazycolumn to scroll it to top.

val scrollState = rememberLazyListState()

LazyColumn( state = scrollState){}

LaunchedEffect(true) {
        scrollState.animateScrollToItem(0)
    }   

CodePudding user response:

I've just put the LaunchEffect within the if block and the issue have been resolved. Here is the code:

    scrollToTop?.getContentIfNotHandled()?.let { scroll ->
        if (scroll) {
            LaunchedEffect(key1 = Unit) {
                listState.scrollToItem(0)
            }
        }
    }

  • Related