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

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 显示旧数据。

排查思路

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

相关推荐
禁止摆烂_才浅1 小时前
前端 AI 面试题
前端·面试·ai编程
程序员老赵1 小时前
Docker 部署 ZLMediaKit:轻松搭建高性能流媒体服务平台
前端·javascript·后端
Liora_Yvonne1 小时前
为什么每次发版,总有用户看到白屏?
前端
风月说与山鬼1 小时前
四、浏览器存储
前端·vue.js
liuxiaocheng1 小时前
上下文工程:LangChain 怎么组织「喂给模型的东西」
前端·后端
breeze jiang1 小时前
CSS 三栏布局怎么写:Flex、Grid 完整方案与 BFC 区别
前端·css
阿懂在掘金1 小时前
记录四年前的一次前端项目权限方案改造
前端·vue.js·架构
郭邯1 小时前
手写一个Cron表达式解析器:从需求分析到AI辅助实现
前端
阿黎梨梨1 小时前
Docker 容器化实战:从零搭建 Web 服务与反向代理
前端·后端·docker