首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性

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

原创
作者头像
hunter android
修改2026-08-19 10:21:58
修改2026-08-19 10:21:58
990
举报

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

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


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

1.1 基础场景

代码语言:javascript
复制
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 页

代码语言:javascript
复制
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 真正可见时加载数据,避免预加载浪费资源。

错误方案

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

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

代码语言:javascript
复制
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 选中状态

代码语言:javascript
复制
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 预加载范围控制

代码语言:javascript
复制
// 设置预加载页数
binding.viewPager.offscreenPageLimit = 1 // 默认值

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

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

3.2 动态调整策略

代码语言:javascript
复制
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

代码语言:javascript
复制
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 自身状态保存

代码语言:javascript
复制
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 动态数据场景的陷阱

代码语言:javascript
复制
// ❌ 错误:数据变化后 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

代码语言:javascript
复制
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 场景

代码语言:javascript
复制
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 解决方案

代码语言:javascript
复制
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 崩溃

现象

代码语言:javascript
复制
java.lang.IllegalStateException: Fragment not attached to a context.

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

解决

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Android ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性
    • 一、ViewPager2 与 Fragment 的生命周期差异
      • 1.1 基础场景
      • 1.2 生命周期时序
    • 二、懒加载与可见性判断
      • 2.1 问题场景
      • 2.2 正确方案:结合 lifecycle 与可见性
      • 2.3 更精确的方案:监听 ViewPager 选中状态
    • 三、offscreenPageLimit 的边界
      • 3.1 预加载范围控制
      • 3.2 动态调整策略
    • 四、状态保存与恢复
      • 4.1 问题场景
      • 4.2 使用 SavedStateHandle
      • 4.3 Fragment 自身状态保存
    • 五、FragmentStateAdapter 的复用与缓存
      • 5.1 Fragment 复用机制
      • 5.2 动态数据场景的陷阱
      • 5.3 正确方案:覆盖 getItemId
    • 六、嵌套 ViewPager2 的生命周期问题
      • 6.1 场景
      • 6.2 问题
      • 6.3 解决方案
    • 七、线上问题排查
      • 7.1 Fragment not attached 崩溃
      • 7.2 Fragment 状态不一致
    • 八、最佳实践总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档