[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 的生命周期协同机制,可以避免常见陷阱,构建稳定流畅的页面滑动体验。

相关文章
监控 前端开发 Java
22 0
人工智能 JavaScript 测试技术
212 4
|
缓存 编解码 Android开发
Android内存优化之图片优化
本文主要探讨Android开发中的图片优化问题,包括图片优化的重要性、OOM错误的成因及解决方法、Android支持的图片格式及其特点。同时介绍了图片储存优化的三种方式:尺寸优化、质量压缩和内存重用,并详细讲解了相关的实现方法与属性。此外,还分析了图片加载优化策略,如异步加载、缓存机制、懒加载等,并结合多级缓存流程提升性能。最后对比了几大主流图片加载框架(Universal ImageLoader、Picasso、Glide、Fresco)的特点与适用场景,重点推荐Fresco在处理大图、动图时的优异表现。这些内容为开发者提供了全面的图片优化解决方案。
557 1
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
14138 1
|
存储 XML Java
Android 文件数据储存之内部储存 + 外部储存
简介:本文详细介绍了Android内部存储与外部存储的使用方法及核心原理。内部存储位于手机内存中,默认私有,适合存储SharedPreferences、SQLite数据库等重要数据,应用卸载后数据会被清除。外部存储包括公共文件和私有文件,支持SD卡或内部不可移除存储,需申请权限访问。文章通过代码示例展示了如何保存、读取、追加、删除文件以及将图片保存到系统相册的操作,帮助开发者理解存储机制并实现相关功能。
3131 2
|
开发者 黑灰产治理
阿里云开发者社区积分细则
阿里云开发者社区,积分规则、领取、过期等相关说明
3800 19
算法 Serverless 开发工具
25 0
人工智能 算法 API
24 0
人工智能 前端开发 测试技术
17 0
JSON 自然语言处理 小程序
29 0

热门文章

最新文章