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

相关文章
人工智能 缓存 前端开发
6361 22
人工智能 JavaScript 开发工具
3368 6
缓存 JavaScript Shell
1585 2
开发工具 Swift git
1228 1
Shell API 调度
900 2
|
14天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
2143 121
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
|
15天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1817 13
安全 机器人 API
660 2
缓存 人工智能 算法
743 1

热门文章

最新文章