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

如何让Jetpack Compose ViewPager页面仅在可见时调用一次?

Jetpack Compose HorizontalPager 引导页组件重复加载及OOM问题解决

问题说明

使用Jetpack Compose的HorizontalPager开发引导页时,出现TutorialPager和TutorialPage频繁重组、重复执行的问题,导致页面内的Lottie动画重复加载,最终引发OutOfMemory错误。从日志可见,页面初始化和切换时组件会多次触发,日志重复输出,期望实现页面仅在可见状态下加载一次。

核心原因

  1. HorizontalPager预加载机制:默认会预加载相邻页面,导致非可见页面的Composable提前创建。
  2. 重组触发频繁:TutorialPage内每次重组都会创建新的MutableTransitionState,导致AnimatedVisibility反复执行动画逻辑。
  3. Lottie动画逻辑冗余:同时使用LottieAnimatable和animateLottieCompositionAsState,导致动画重复启动、资源重复加载。

解决方案

1. 修改HorizontalPager配置,关闭预加载

@OptIn(ExperimentalPagerApi::class)
@Composable
fun TutorialPager(
    tutorials: List<TutorialViewModel.TutorialUiResources>,
    state: PagerState,
    modifier: Modifier = Modifier
) {
    HorizontalPager(
        count = tutorials.size,
        state = state,
        modifier = modifier.fillMaxSize(),
        userScrollEnabled = false,
        // 关闭预加载,仅加载当前可见页面
        beyondBoundsPageCount = 0,
        pageSpacing = 0.dp
    ) { page ->
        // 直接使用Pager传入的page索引,避免state.currentPage引发的重组不一致
        TutorialPage(page = page, tutorial = tutorials[page], pagerState = state)
    }
}

2. 优化TutorialPage,减少不必要重组

@OptIn(ExperimentalPagerApi::class)
@Composable
fun TutorialPage(
    page: Int,
    tutorial: TutorialViewModel.TutorialUiResources,
    pagerState: PagerState,
    modifier: Modifier = Modifier
) {
    val isVisible = pagerState.currentPage == page
    val pxValue = LocalDensity.current.run { 60.dp.toPx() }

    // 用remember保存过渡状态,避免每次重组创建新实例
    val lottieVisibleState = remember { MutableTransitionState(false) }
    val titleVisibleState = remember { MutableTransitionState(false) }
    val spacerVisibleState = remember { MutableTransitionState(false) }
    val subTitleVisibleState = remember { MutableTransitionState(false) }

    // 监听页面可见状态,批量更新过渡目标状态
    LaunchedEffect(isVisible) {
        lottieVisibleState.targetState = isVisible
        titleVisibleState.targetState = isVisible
        spacerVisibleState.targetState = isVisible
        subTitleVisibleState.targetState = isVisible
    }

    if (isVisible) {
        Box(modifier = modifier.fillMaxSize()) {
            AnimatedVisibility(
                visibleState = lottieVisibleState,
                enter = pageItemSlideInTransition(pxValue.toInt(), 100)
            ) {
                Log.d("aos", "World: page:$page, ${tutorial.lottie}")
                Lottie(
                    play = isVisible,
                    lottie = tutorial.lottie,
                    modifier = Modifier
                        .align(Alignment.TopStart)
                        .size(220.dp),
                    speed = 0.3f
                )
            }

            Column(
                modifier = Modifier
                    .align(Alignment.BottomStart)
            ) {
                AnimatedVisibility(
                    visibleState = titleVisibleState,
                    enter = pageItemSlideInTransition(pxValue.toInt(), 250)
                ) {
                    Text(
                        text = stringResource(id = tutorial.titleRes),
                        fontFamily = HanSansFamily,
                        style = MaterialTheme.typography.headlineLarge,
                        color = Color.Black
                    )
                }

                AnimatedVisibility(
                    visibleState = spacerVisibleState,
                    enter = pageItemSlideInTransition(pxValue.toInt(), 400)
                ) {
                    Spacer(
                        modifier = Modifier
                            .padding(vertical = 20.dp)
                            .size(width = 28.dp, height = 4.dp)
                            .background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(100))
                    )
                }

                AnimatedVisibility(
                    visibleState = subTitleVisibleState,
                    enter = pageItemSlideInTransition(pxValue.toInt(), 550)
                ) {
                    Text(
                        text = stringResource(id = tutorial.subTitleRes),
                        style = MaterialTheme.typography.bodyLarge,
                        lineHeight = 22.4.sp,
                        color = Color.Gray05
                    )
                }
            }
        }
    }
}

3. 简化Lottie组件,避免资源重复加载

@Composable
fun Lottie(play: Boolean, @RawRes lottie: Int, modifier: Modifier = Modifier, speed: Float = 1f, iterations: Int = LottieConstants.IterateForever) {
    // rememberLottieComposition确保资源仅加载一次
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(lottie))

    // 用animateLottieCompositionAsState统一控制播放状态,避免冗余逻辑
    val progress by animateLottieCompositionAsState(
        composition = composition,
        isPlaying = play,
        speed = speed,
        iterations = iterations,
        restartOnPlay = true
    )

    LottieAnimation(
        composition = composition,
        progress = { progress },
        modifier = modifier
    )
}

修改说明

  • 关闭预加载:通过beyondBoundsPageCount = 0禁止Pager提前创建非可见页面的Composable,从源头减少组件实例。
  • 稳定过渡状态:用remember保存MutableTransitionState,避免每次重组都触发动画重新执行。
  • 简化Lottie逻辑:移除冗余的LottieAnimatable,仅通过animateLottieCompositionAsState控制播放,确保资源只加载一次,动画仅在页面可见时运行。

内容的提问来源于stack exchange,提问作者c-an

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 23:05:23