Android Paging Compose:查询条件变更时如何滚动到列表顶部?
问题描述
我有一个商品列表,在ProductsScreen界面中设置了筛选选项、搜索框以及筛选芯片:
我使用Paging 3库实现分页功能,希望在搜索查询或选中的筛选芯片发生变更时,列表能够自动滚动到顶部。我已经在搜索查询或筛选芯片变更时将scrollToTop设为true,并在ProductsScreen中监听,但该逻辑并未生效。
下面是我的代码:
ProductsScreen代码
@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()) } } } } } } } }
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) } } }
解决方案
你的问题出在LaunchedEffect的触发时机和监听逻辑上,以下是修复方案:
1. 修正LaunchedEffect的监听逻辑
原来的LaunchedEffect(key1 = Unit)只会在Composable首次组合时执行一次,后续scrollToTop的变化不会触发它重新执行。你需要把scrollToTop作为LaunchedEffect的key,同时等待Paging数据加载完成后再执行滚动:
// 替换原来的LaunchedEffect代码 LaunchedEffect(scrollToTop) { viewModel.scrollToTop.asFlow().collect { event -> event.getContentIfNotHandled()?.let { shouldScroll -> if (shouldScroll) { // 监听Paging的刷新状态,等待数据加载完成后再滚动 products.loadState.refresh.collect { loadState -> if (loadState is LoadState.NotLoading) { listState.scrollToItem(0) } } } } } }
2. 问题根源说明
LaunchedEffect(key1 = Unit)仅执行一次,后续scrollToTop更新时不会重新触发,导致无法响应新的滚动指令。- 直接调用
scrollToItem(0)可能在Paging数据还没刷新完成时执行,此时列表无数据或未完成重组,滚动指令无效。
3. 可选优化:统一使用StateFlow
如果可以,建议把_scrollToTop改成MutableStateFlow,和其他状态流保持一致,Compose中使用Flow会更顺畅:
在ViewModel中修改:
private val _scrollToTop = MutableStateFlow<Event<Boolean>>(Event(false)) val scrollToTop: StateFlow<Event<Boolean>> get() = _scrollToTop // 触发滚动时的调用逻辑不变,依然设置为Event(true) _scrollToTop.value = Event(true)
在Composable中收集:
val scrollToTop by viewModel.scrollToTop.collectAsState() LaunchedEffect(scrollToTop) { scrollToTop.getContentIfNotHandled()?.let { shouldScroll -> if (shouldScroll) { products.loadState.refresh.collect { loadState -> if (loadState is LoadState.NotLoading) { listState.scrollToItem(0) } } } } }
内容的提问来源于stack exchange,提问作者Jarnojr
相关产品推荐
相关产品推荐

