You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中Paging3状态异常及Lazy布局卡顿问题求助

Jetpack Compose 动漫索引应用问题排查与解决方案

问题1:Paging状态无法正确恢复,始终停留在第一页末尾

原因分析

  • LazyState 作用域错误:当前在ExplorationScreen内部用remember创建的LazyGridState/LazyListState仅在当前Composition生命周期内有效,导航离开或配置变化时会被销毁,无法持久化。
  • Paging Flow 切换重置:使用flatMapLatest切换displayedAnimeType时,会取消前一个Flow并创建新流,导致之前的分页状态丢失;同时将anime Flow嵌入ExploreScreenState的方式,无法复用不同类型的缓存状态。
  • LazyPagingItems 重复创建:每次切换displayType或页面重组时,collectAsLazyPagingItems()会生成新实例,丢失滚动和分页状态。

解决方案

步骤1:在ViewModel中持久化滚动状态

为每个DisplayType和DisplayStyle保存滚动位置,避免Composition销毁时丢失:

class ExploreViewModel : ViewModel() {
    private val scrollStates = mutableMapOf<Pair<DisplayType, DisplayStyle>, ScrollState>()

    data class ScrollState(
        val listFirstVisibleItemIndex: Int = 0,
        val listFirstVisibleItemOffset: Int = 0,
        val gridFirstVisibleItemIndex: Int = 0,
        val gridFirstVisibleItemOffset: Int = 0
    )

    fun saveScrollState(type: DisplayType, style: DisplayStyle, state: LazyListState) {
        scrollStates[Pair(type, style)] = scrollStates[Pair(type, style)]?.copy(
            listFirstVisibleItemIndex = state.firstVisibleItemIndex,
            listFirstVisibleItemOffset = state.firstVisibleItemScrollOffset
        ) ?: ScrollState(
            listFirstVisibleItemIndex = state.firstVisibleItemIndex,
            listFirstVisibleItemOffset = state.firstVisibleItemScrollOffset
        )
    }

    fun saveScrollState(type: DisplayType, style: DisplayStyle, state: LazyGridState) {
        scrollStates[Pair(type, style)] = scrollStates[Pair(type, style)]?.copy(
            gridFirstVisibleItemIndex = state.firstVisibleItemIndex,
            gridFirstVisibleItemOffset = state.firstVisibleItemScrollOffset
        ) ?: ScrollState(
            gridFirstVisibleItemIndex = state.firstVisibleItemIndex,
            gridFirstVisibleItemOffset = state.firstVisibleItemScrollOffset
        )
    }

    fun getScrollState(type: DisplayType, style: DisplayStyle): ScrollState {
        return scrollStates.getOrDefault(Pair(type, style), ScrollState())
    }
}

步骤2:为每个DisplayType单独缓存Paging Flow

避免切换类型时重置状态,复用已有的Flow缓存:

class ExploreViewModel : ViewModel() {
    private val animeFlows = mutableMapOf<DisplayType, Flow<PagingData<Anime>>>()

    private val anime: Flow<PagingData<Anime>> = displayedAnimeType.flatMapLatest { type ->
        animeFlows.getOrPut(type) {
            when (type) {
                DisplayType.POPULAR -> useCases.getPopularAnime().cachedIn(viewModelScope)
                DisplayType.AIRING -> useCases.getAiringAnime().cachedIn(viewModelScope)
                DisplayType.UPCOMING -> useCases.getUpcomingAnime().cachedIn(viewModelScope)
                DisplayType.SEARCH -> searchedAnime.cachedIn(viewModelScope)
            }
        }
    }

    // 其余代码保持不变
}

步骤3:在Composable中恢复滚动状态

结合rememberSaveable和ViewModel保存的状态,恢复滚动位置:

fun ExplorationScreen(
    screenState: ExploreScreenState,
    viewModel: ExploreViewModel,
    onEvent: (ExploreScreenEvent) -> Unit,
    onAnimeClicked: (Anime) -> Unit,
    modifier: Modifier = Modifier
) {
    val pagingAnime: LazyPagingItems<Anime> = screenState.anime.collectAsLazyPagingItems()
    val scrollState = viewModel.getScrollState(screenState.displayType, screenState.displayStyle)

    val lazyListState = rememberSaveable(screenState.displayType, screenState.displayStyle) {
        LazyListState(
            firstVisibleItemIndex = scrollState.listFirstVisibleItemIndex,
            firstVisibleItemScrollOffset = scrollState.listFirstVisibleItemOffset
        )
    }

    val lazyGridState = rememberSaveable(screenState.displayType, screenState.displayStyle) {
        LazyGridState(
            firstVisibleItemIndex = scrollState.gridFirstVisibleItemIndex,
            firstVisibleItemScrollOffset = scrollState.gridFirstVisibleItemOffset
        )
    }

    // 监听滚动变化并保存到ViewModel
    LaunchedEffect(lazyListState) {
        snapshotFlow { lazyListState.firstVisibleItemIndex to lazyListState.firstVisibleItemScrollOffset }
            .distinctUntilChanged()
            .collect { (index, offset) ->
                viewModel.saveScrollState(screenState.displayType, screenState.displayStyle, lazyListState)
            }
    }

    LaunchedEffect(lazyGridState) {
        snapshotFlow { lazyGridState.firstVisibleItemIndex to lazyGridState.firstVisibleItemScrollOffset }
            .distinctUntilChanged()
            .collect { (index, offset) ->
                viewModel.saveScrollState(screenState.displayType, screenState.displayStyle, lazyGridState)
            }
    }

    // 其余Scaffold代码保持不变
}

问题2:Lazy布局流畅度不如XML实现

原因分析

  • 图片加载未优化:封面图加载无占位符、低分辨率预览,或缓存策略不合理,导致滚动卡顿。
  • 不必要的重组:Lazy Item未使用稳定类型,或未设置唯一key,滚动时频繁触发重组。
  • LoadState 处理不当:当前直接return@Scaffold的方式会导致整个布局频繁重组。
  • 缺乏性能优化配置:未开启R8混淆,或未使用性能工具定位瓶颈。

解决方案

步骤1:优化图片加载(以Coil为例)

添加占位符、错误图,配置合理缓存:

AsyncImage(
    model = anime.coverImage,
    contentDescription = anime.title,
    modifier = Modifier.size(120.dp),
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error_placeholder),
    contentScale = ContentScale.Crop,
    imageLoader = LocalImageLoader.current.newBuilder()
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build()
)

步骤2:减少Lazy Item重组

  • 确保Anime数据类为稳定类型(添加@Stable注解,或使用Kotlin数据类默认特性)。
  • 为Item设置唯一key:
// LazyListAnime 示例
LazyColumn(state = lazyListState) {
    items(pagingAnime, key = { it.id }) { anime ->
        anime?.let { AnimeListItem(anime = it, onAnimeClicked = onAnimeClicked) }
    }
    // 加载更多状态项
}

// LazyGridAnime 示例
LazyVerticalGrid(columns = GridCells.Fixed(2), state = lazyGridState) {
    items(pagingAnime, key = { it.id }) { anime ->
        anime?.let { AnimeGridItem(anime = it, onAnimeClicked = onAnimeClicked) }
    }
    // 加载更多状态项
}

步骤3:优化LoadState处理方式

改用条件组合布局,减少整体布局重组:

Scaffold(
    topBar = { /* Top App Bar */ },
    modifier = modifier
) { paddingValue ->
    Box(modifier = Modifier.padding(paddingValue)) {
        when {
            pagingAnime.itemCount == 0 && screenState.displayType == DisplayType.SEARCH -> {
                EmptySearchState()
            }
            pagingAnime.loadState.refresh is LoadState.Error -> {
                ErrorState(onRetry = { pagingAnime.retry() })
            }
            pagingAnime.loadState.refresh is LoadState.Loading -> {
                LoadingState()
            }
            else -> {
                if (screenState.displayStyle == DisplayStyle.LIST) {
                    LazyListAnime(/* 参数 */)
                } else {
                    LazyGridAnime(/* 参数 */)
                }
            }
        }

        // 底部加载更多状态
        if (pagingAnime.loadState.append is LoadState.Loading) {
            CircularProgressIndicator(modifier = Modifier.align(Alignment.BottomCenter))
        }
    }
}

步骤4:启用性能优化配置

  • 在build.gradle中开启R8混淆:
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
  • 使用Android Studio的Layout Inspector和Compose Profiler定位重组瓶颈与耗时操作。

内容的提问来源于stack exchange,提问作者lelestacia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 15:47:01