Android ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性
ViewPager2 + Fragment 是 Android 应用中常见的页面滑动方案,但在实际项目中,Fragment 的生命周期、预加载行为、状态保存与恢复往往带来棘手问题。本文从生命周期协同到状态一致性,构建可靠的 ViewPager2 方案。
一、ViewPager2 与 Fragment 的生命周期差异
1.1 基础场景
kotlin
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 页
scss
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 真正可见时加载数据,避免预加载浪费资源。
错误方案:
kotlin
// ❌ onResume 会在 ViewPager 切换时多次触发
override fun onResume() {
super.onResume()
loadData() // 每次切换回来都会重复加载
}
2.2 正确方案:结合 lifecycle 与可见性
kotlin
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 选中状态
kotlin
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 预加载范围控制
kotlin
// 设置预加载页数
binding.viewPager.offscreenPageLimit = 1 // 默认值
影响:
offscreenPageLimit = 1:当前页 + 左右各 1 页保持在内存中offscreenPageLimit = 2:当前页 + 左右各 2 页offscreenPageLimit = ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT(-1):等同于 1
注意:
- 增大
offscreenPageLimit会增加内存占用 - 过小的值会导致频繁重建 Fragment,影响流畅度
3.2 动态调整策略
kotlin
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
kotlin
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 自身状态保存
kotlin
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 动态数据场景的陷阱
kotlin
// ❌ 错误:数据变化后 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
kotlin
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 场景
scss
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 解决方案
kotlin
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 崩溃
现象:
arduino
java.lang.IllegalStateException: Fragment not attached to a context.
原因 :Fragment 已销毁,但异步任务仍在执行并访问 requireContext()。
解决:
kotlin
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 显示旧数据。
排查思路:
- 检查
getItemId()是否返回稳定的唯一 ID - 确认
notifyDataSetChanged()是否在数据变化后调用 - 检查 ViewModel 是否正确共享或隔离
八、最佳实践总结
| 场景 | 推荐方案 |
|---|---|
| 懒加载 | onResume + isFirstLoad 标志位 |
| 精确可见性 | OnPageChangeCallback + onPageVisible() |
| 状态保存 | SavedStateHandle + onSaveInstanceState |
| 动态数据 | 覆盖 getItemId() 和 containsItem() |
| 嵌套 ViewPager | 监听父 Fragment 生命周期 |
| 异步任务 | 使用 viewLifecycleOwner.lifecycleScope |
工程化建议:
- 统一封装
BaseTabFragment,处理懒加载逻辑 - 在 ViewModel 中管理数据状态,而非 Fragment
- 记录线上
Fragment not attached崩溃,建立监控告警 - 使用
FragmentScenario编写单元测试,覆盖生命周期边界
通过理解 ViewPager2 与 Fragment 的生命周期协同机制,可以避免常见陷阱,构建稳定流畅的页面滑动体验。