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)
}
}
}