[Android 从零到一] ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性

简介: 本文深入解析 ViewPager2 与 Fragment 的生命周期协同机制,涵盖预加载行为、懒加载实现、`offscreenPageLimit` 调优、状态保存恢复、动态数据适配及嵌套滑动等核心问题,提供可落地的最佳实践与线上排障方案。

Android ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性

ViewPager2 + Fragment 是 Android 应用中常见的页面滑动方案,但在实际项目中,Fragment 的生命周期、预加载行为、状态保存与恢复往往带来棘手问题。本文从生命周期协同到状态一致性,构建可靠的 ViewPager2 方案。


一、ViewPager2 与 Fragment 的生命周期差异

1.1 基础场景

class MainFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val binding = FragmentMainBinding.inflate(inflater, container, false)

        val adapter = ViewPagerAdapter(this)
        binding.viewPager.adapter = adapter

        return binding.root
    }
}

class ViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount() = 3

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> TabFragment.newInstance("Tab 1")
            1 -> TabFragment.newInstance("Tab 2")
            2 -> TabFragment.newInstance("Tab 3")
            else -> throw IllegalArgumentException()
        }
    }
}

1.2 生命周期时序

场景:用户首次进入,ViewPager 显示第 0 页

MainFragment.onCreateView
TabFragment(0).onCreate
TabFragment(0).onCreateView
TabFragment(0).onViewCreated
TabFragment(0).onStart
TabFragment(0).onResume

// 预加载 position=1(默认 offscreenPageLimit=1)
TabFragment(1).onCreate
TabFragment(1).onCreateView
TabFragment(1).onViewCreated
TabFragment(1).onStart

关键点

  • position=1 的 Fragment 执行到 onStart,但不会执行 onResume
  • 用户滑动到 position=1 时,才触发 TabFragment(1).onResume
  • position=2 在用户滑动到 position=1 后才开始创建

二、懒加载与可见性判断

2.1 问题场景

需求:只在 Fragment 真正可见时加载数据,避免预加载浪费资源。

错误方案

// ❌ onResume 会在 ViewPager 切换时多次触发
override fun onResume() {
    super.onResume()
    loadData() // 每次切换回来都会重复加载
}

2.2 正确方案:结合 lifecycle 与可见性

class TabFragment : Fragment() {
    private var isFirstLoad = true

    override fun onResume() {
        super.onResume()
        if (isFirstLoad) {
            loadData()
            isFirstLoad = false
        }
    }

    private fun loadData() {
        viewLifecycleOwner.lifecycleScope.launch {
            val data = repository.fetchData()
            binding.textView.text = data
        }
    }
}

2.3 更精确的方案:监听 ViewPager 选中状态

class MainFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                // 通知 Fragment 可见性变化
                val fragment = childFragmentManager.findFragmentByTag("f$position") as? TabFragment
                fragment?.onPageVisible()
            }
        })
    }
}

class TabFragment : Fragment() {
    private var hasLoadedData = false

    fun onPageVisible() {
        if (!hasLoadedData && isResumed) {
            loadData()
            hasLoadedData = true
        }
    }
}

三、offscreenPageLimit 的边界

3.1 预加载范围控制

// 设置预加载页数
binding.viewPager.offscreenPageLimit = 1 // 默认值

影响

  • offscreenPageLimit = 1:当前页 + 左右各 1 页保持在内存中
  • offscreenPageLimit = 2:当前页 + 左右各 2 页
  • offscreenPageLimit = ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT(-1):等同于 1

注意

  • 增大 offscreenPageLimit 会增加内存占用
  • 过小的值会导致频繁重建 Fragment,影响流畅度

3.2 动态调整策略

class MainFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 根据内存情况动态调整
        val activityManager = requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val memoryInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memoryInfo)

        binding.viewPager.offscreenPageLimit = if (memoryInfo.lowMemory) {
            1 // 低内存时减少预加载
        } else {
            2 // 正常情况预加载更多
        }
    }
}

四、状态保存与恢复

4.1 问题场景

现象:用户在 TabFragment(0) 填写表单,切换到其他页面后返回,输入内容丢失。

原因:Fragment 被销毁后未正确保存状态。

4.2 使用 SavedStateHandle

class TabFragment : Fragment() {
    private val viewModel: TabViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 恢复输入状态
        viewModel.inputText.observe(viewLifecycleOwner) { text ->
            if (binding.editText.text.toString() != text) {
                binding.editText.setText(text)
            }
        }

        binding.editText.addTextChangedListener { editable ->
            viewModel.updateInputText(editable?.toString() ?: "")
        }
    }
}

class TabViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val inputText: LiveData<String> = savedStateHandle.getLiveData("input_text", "")

    fun updateInputText(text: String) {
        savedStateHandle["input_text"] = text
    }
}

4.3 Fragment 自身状态保存

class TabFragment : Fragment() {
    private var scrollPosition = 0

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("scroll_position", binding.recyclerView.computeVerticalScrollOffset())
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        savedInstanceState?.let {
            scrollPosition = it.getInt("scroll_position", 0)
            binding.recyclerView.post {
                binding.recyclerView.scrollBy(0, scrollPosition)
            }
        }
    }
}

五、FragmentStateAdapter 的复用与缓存

5.1 Fragment 复用机制

FragmentStateAdapter 内部使用 FragmentManager 管理 Fragment:

  • 已创建的 Fragment 通过 tag 缓存(格式:f{itemId}
  • getItemId(position) 返回唯一 ID,默认实现是 position.toLong()

5.2 动态数据场景的陷阱

// ❌ 错误:数据变化后 position 对应的内容变了,但 Fragment 没有更新
class DynamicAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    private var items = listOf<String>()

    fun updateItems(newItems: List<String>) {
        items = newItems
        notifyDataSetChanged() // 不会触发 Fragment 重建
    }

    override fun getItemCount() = items.size

    override fun createFragment(position: Int): Fragment {
        return TabFragment.newInstance(items[position])
    }
}

问题notifyDataSetChanged() 不会重建已有的 Fragment。

5.3 正确方案:覆盖 getItemId

class DynamicAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    private var items = listOf<Item>()

    fun updateItems(newItems: List<Item>) {
        items = newItems
        notifyDataSetChanged()
    }

    override fun getItemCount() = items.size

    override fun getItemId(position: Int): Long {
        return items[position].id // 使用数据的唯一 ID
    }

    override fun containsItem(itemId: Long): Boolean {
        return items.any { it.id == itemId }
    }

    override fun createFragment(position: Int): Fragment {
        return TabFragment.newInstance(items[position].title)
    }
}

六、嵌套 ViewPager2 的生命周期问题

6.1 场景

ViewPager2 (外层)
  - TabFragment(0)
      - ViewPager2 (内层)
          - InnerFragment(0)
          - InnerFragment(1)
  - TabFragment(1)

6.2 问题

内层 InnerFragment 的生命周期受外层 TabFragment 影响:

  • 外层切换到 TabFragment(1) 时,TabFragment(0) 执行 onPause
  • 内层 InnerFragment(0) 也会 onPause
  • 用户切换回 TabFragment(0) 后,InnerFragment(0) 再次 onResume

6.3 解决方案

class InnerFragment : Fragment() {
    private var isPausedByParent = false

    override fun onPause() {
        super.onPause()
        isPausedByParent = !requireActivity().isChangingConfigurations
    }

    override fun onResume() {
        super.onResume()
        if (isPausedByParent) {
            // 恢复数据刷新
            resumeDataRefresh()
        }
        isPausedByParent = false
    }
}

七、线上问题排查

7.1 Fragment not attached 崩溃

现象

java.lang.IllegalStateException: Fragment not attached to a context.

原因:Fragment 已销毁,但异步任务仍在执行并访问 requireContext()

解决

class TabFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            val data = repository.fetchData()
            // ✅ 使用 viewLifecycleOwner,生命周期结束时自动取消
            if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
                binding.textView.text = data
            }
        }
    }
}

7.2 Fragment 状态不一致

现象:切换页面后,部分 Fragment 显示旧数据。

排查思路

  1. 检查 getItemId() 是否返回稳定的唯一 ID
  2. 确认 notifyDataSetChanged() 是否在数据变化后调用
  3. 检查 ViewModel 是否正确共享或隔离

八、最佳实践总结

场景 推荐方案
懒加载 onResume + isFirstLoad 标志位
精确可见性 OnPageChangeCallback + onPageVisible()
状态保存 SavedStateHandle + onSaveInstanceState
动态数据 覆盖 getItemId()containsItem()
嵌套 ViewPager 监听父 Fragment 生命周期
异步任务 使用 viewLifecycleOwner.lifecycleScope

工程化建议

  • 统一封装 BaseTabFragment,处理懒加载逻辑
  • 在 ViewModel 中管理数据状态,而非 Fragment
  • 记录线上 Fragment not attached 崩溃,建立监控告警
  • 使用 FragmentScenario 编写单元测试,覆盖生命周期边界

通过理解 ViewPager2 与 Fragment 的生命周期协同机制,可以避免常见陷阱,构建稳定流畅的页面滑动体验。

相关文章
|
27天前
|
监控 前端开发 Java
[鸿蒙从零到一] HarmonyOS 日志、崩溃分析与调试实战:从问题定位到线上治理
本文系统讲解HarmonyOS日志、崩溃分析与调试实战:涵盖HiLog分级日志、全局崩溃捕获、DevEco断点调试、hilog命令行工具、内存泄漏排查及线上治理(脱敏、采样、分级上报),助力开发者构建高稳定性应用。
91 0
|
机器学习/深度学习 资源调度 算法
【机器学习基础】对数几率回归(logistic回归)
【机器学习基础】对数几率回归(logistic回归)
1311 0
|
27天前
|
JSON 人工智能 Java
【AI】Agent 全栈进阶|工具调用与结构化输出
文章介绍了大模型的关键能力——Function Calling(函数调用)与结构化输出,主要包含四部分内容: Function Calling 原理,工具定义与注册,JSON Schema 约束输出,最小工具调用循环
138 2
|
27天前
|
人工智能 数据安全/隐私保护 自然语言处理
阿里云百炼AI通用型节省计划、资源包、Token Plan三种计费方式详解与选型指南
本文介绍了阿里云百炼平台三大核心计费模式的底层差异与选型策略。AI通用型节省计划通过承诺月消费换取阶梯折扣,最高5.3折,覆盖阿里直供全模型,适合长期稳定的多模型混合使用场景;资源包为预付费固定资源量方案,仅支持单一指定模型,灵活性低,适配短期测试、单一模型轻量使用场景;Token Plan采用统一Credits订阅制,全模型通用且支持团队席位管理,成本可控,适合新用户入门试水。文章结合抵扣优先级、适用场景与最新优惠活动,为不同规模的企业和开发者提供精准降本选型指南。
阿里云百炼AI通用型节省计划、资源包、Token Plan三种计费方式详解与选型指南
|
27天前
|
JSON 自然语言处理 小程序
节假日查询-假期信息查询 API 接口文档教程
本文为开发者与系统工程师提供阿里云「法定节假日查询」API的权威接入指南,涵盖单参数调用、调休补班识别、多语言示例、免费试用及透明计费等核心能力,助力电商、HR、金融、ERP等场景快速实现假期自动化识别。
181 0
节假日查询-假期信息查询 API 接口文档教程
|
27天前
|
Arthas 监控 Java
阿里程序员常用的 15 款开发者工具!
阿里巴巴将自身在各类业务场景下的技术积淀,通过开源、云上实现或工具等形式对外开放,本文将精选了一些阿里巴巴的开发者工具,希望能帮助开发者们提高开发效率、更优雅的写代码。
280 1
|
6月前
|
人工智能 安全 前端开发
Team 版 OpenClaw:HiClaw 开源,5 分钟完成本地安装
HiClaw 基于 OpenClaw、Higress AI Gateway、Element IM 客户端+Tuwunel IM 服务器(均基于 Matrix 实时通信协议)、MinIO 共享文件系统打造。
11629 55
|
27天前
|
搜索推荐 Linux API
趁 DeepSeek Harness 刚火,我做了一个 DSH 插件市场,2 天冷启动记录
DeepSeek Harness 插件生态分散难用?DSH Marketplace(dshmarketplace.dev)应运而生:聚合插件、标注兼容性、提供一键安装。支持Web/CLI/Python/API/DSH内置Store等多入口,含自托管插件。开源共建,欢迎用户反馈与作者入驻。(239字)
|
11月前
|
人工智能 监控 数据可视化
什么是低代码开发平台?2025年最热门的10大低代码开发平台盘点!
低代码开发平台通过可视化拖拽、模型驱动等方式,大幅减少手工编码,提升应用开发效率。当下更是结合AI能力,自动生成应用,组件,图表,进一步加快应用软件的开发效率落地。
|
JavaScript
vite-plugin-html的使用及实现
【8月更文挑战第4天】vite-plugin-html的使用及实现(实现一个简易版的插件)
1963 4