news 2026/10/9 4:40:59

ViewPager 预加载机制与 Fragment 懒加载实战:从 setOffscreenPageLimit 原理到 BaseLazyFragment 完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ViewPager 预加载机制与 Fragment 懒加载实战:从 setOffscreenPageLimit 原理到 BaseLazyFragment 完整实现
  • 教程
  • 技术博客
  • 文档

【免费下载链接】YCBlogs

技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!

项目地址:https://gitcode.com/gh_mirrors/yc/YCBlogs
点击查看免费下载

导读

本篇文章基于 YCBlogs 仓库中的《03.ViewPager懒加载.md》一文展开,系统梳理 ViewPager 的预加载机制、setOffscreenPageLimit的源码限制、Fragment 懒加载的核心思想,并给出可直接落地的BaseLazyFragment与配合状态管理器的BaseStateFragment完整实现。阅读完本文,你将理解 ViewPager 与 PagerAdapter 的协作流程,掌握"可见才加载、仅加载一次"的懒加载方案,并能在项目里快速复用这套封装解决"多 Fragment 同时发网络请求导致流量浪费、页面卡顿"的经典问题。

01. ViewPager 与 PagerAdapter 的协作机制

ViewPager 并不直接管理页面 View,而是使用一个键(key)对象来关联每一页。这个键用于追踪并唯一标识 Adapter 中独立位置的某一页。整个协作过程依赖 PagerAdapter 的四个核心回调,ViewPager 通过回调方式通知 PagerAdapter 管理其中的页面:

  • startUpdate(ViewGroup):表明 ViewPager 中的内容需要更改,一次更新的开始。
  • instantiateItem(ViewGroup, int):构造页面视图。通过一次或多次调用该方法来构造需要的页面。
  • destroyItem(ViewGroup, int, Object):取消 ViewPager 关联的页面视图,即销毁不再需要的页面。
  • finishUpdate(ViewGroup):当一次更新(添加和/或移除)完成之后调用,通知 Adapter 提交关联和/或取消关联的操作。

最简单的实现方式是用每页的 View 本身作为 key 来关联它们自己:在instantiateItem(ViewGroup, int)中创建 View 并添加到 ViewGroup 后返回该 View;destroyItem(ViewGroup, int, Object)中将其从 ViewGroup 移除;同时在isViewFromObject(View, Object)中返回return view == object;。

此外,PagerAdapter 支持数据改变时刷新界面。数据改变必须在主线程中调用,并在改变完成后调用notifyDataSetChanged(),这与AdapterView中派生自BaseAdapter的用法相似。一次数据改变可能关联页面的添加、移除或位置改变,ViewPager 会根据 Adapter 中getItemPosition(Object)返回的结果判断是否保留当前已构造的活动页面(重用而非完全重新构造)。

关于 PagerAdapter 更完整的抽象方法说明(instantiateItem、destroyItem、isViewFromObject、getItemPosition等)与 ViewPager 的配合关系,可进一步阅读仓库中的 PagerAdapter详细介绍。

02. ViewPager 弊端分析:为什么需要懒加载

普通 ViewPager 如果你不通过setOffscreenPageLimit(int limit)显式设置预加载数量,默认会加载当前页的左右两页。也就是说,进入 ViewPager 第一页时,第一页和第二页会被一起加载。这带来一个非常现实的问题:

如果我们把setOffscreenPageLimit设置为 3,那么进入 ViewPager 以后会同时加载 4 个 Fragment。像平时项目中的这些 Fragment 一般都会发送网络请求,也就是说 4 个 Fragment 同时发网络请求去获取数据。结果显而易见——用户体验不好,比如浪费用户流量、造成卡顿等等。

而"懒加载"针对 Fragment 实现 PagerAdapter 时还有更深层的矛盾:

  • 概念上,懒加载是"当需要时才加载,加载之后一直保持该对象"。
  • 但FragmentPagerAdapter与FragmentStatePagerAdapter都没有完全保存其引用和状态:前者需要重建视图,后者使用状态恢复,View 都被销毁,只是恢复方式不同。而我们通常希望 Fragment 一旦被加载,其视图也不再被销毁,即不重新走一遍完整生命周期。
  • 同时 ViewPager 为了实现滑动效果,必然预加载左右两侧的页面。

因此我们通常想要实现的两种效果是:不提供滑动时,需要时才构造页面,并且只走一遍生命周期,避免在 Fragment 中做过多的状态保存和恢复。

这种预加载带来的可见性问题在真实场景中很常见。仓库中的仿抖音滑动分页视频一文就明确指出:很多人以为 Fragment 在onResume的时候就是可见的,但 ViewPager 中的 Fragment 恰恰相反,尤其是多个 ViewPager 嵌套时,会同时有多个父 Fragment、多个子 Fragment 处于onResume状态,却只有其中一个是真正可见的。在页面内容曝光等重要的数据上报场景中,就需要同时判断onResumed、setUserVisibleHint、setOnPageChangeListener等多个条件。

03. ViewPager 预加载机制:setOffscreenPageLimit 为什么设 0 无效

既然预加载带来问题,一个直观的想法是:把 ViewPager 的预加载数量设为 0 不就行了?代码这样写:

vp.setOffscreenPageLimit(0);

但看一下 ViewPager 的源码就会明白,这个方法是行不通的:

public void setOffscreenPageLimit(int limit) { if (limit < 1) { Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1); limit = 1; } if (limit != this.mOffscreenPageLimit) { this.mOffscreenPageLimit = limit; this.populate(); } }

源码逻辑非常清晰:即使你设置为 0,方法内部也会判断后把它默认设为 1,所以完全关闭预加载是不可能的。同时可以看到,只有当新值与旧的mOffscreenPageLimit不同时,才会触发populate()重新计算并布局页面。

ViewPager 默认情况下的加载行为是:当切换到当前页面时,会默认预加载左右两侧的布局到 ViewPager 中,尽管两侧的 View 并不可见。由于mOffscreenPageLimit的下限被限制为 1,页面的预加载是不可避免的。以下以mOffscreenPageLimit == 1为例说明缓存过程:

  • 初始化缓存:初始显示第 0 页,mOffscreenPageLimit为 1,因此预加载第 1 页,再往后的页面(第 2、3、4 页)不需要加载。
  • 中间页面缓存:向右滑动到第 2 页时,左右各需要缓存一页,此时第 0 页超出范围需要销毁,第 3 页需要预加载,第 4 页不需要加载。

这也是为什么在实际项目中通常显式设置vp.setOffscreenPageLimit(1)(如仿抖音滑动分页视频中的用法),在保证滑动流畅的同时把预加载范围控制到最小。

04. ViewPager 部分源码:页面加载与销毁的核心流程

理解懒加载的关键在于理解 ViewPager 内部是如何决定"何时构造页面、何时销毁页面"的。以下是文档梳理的核心方法:

04.1 setAdapter(ViewPager)

设置 Adapter 时主要做这几件事:

  • 销毁旧的 Adapter 数据,用新的 Adapter 更新 UI;
  • 清除旧 Adapter,对已加载的 item 调用destroyItem;
  • 将自身滚动到初始位置this.scrollTo(0, 0);
  • 设置 PagerObserver:mAdapter.setViewPagerObserver(mObserver);
  • 调用populate()方法计算并初始化 View;
  • 如果设置了OnAdapterChangeListener,进行回调。

04.2 populate(int newCurrentItem)

这是 ViewPager 中非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁的页面,然后通过调用 Adapter 的instantiateItem和destroyItem两个方法初始化新页面、销毁不需要的页面。流程如下:

  • 根据newCurrentItem和mOffscreenPageLimit计算要加载的 page 页面,计算出startPos和endPos;
  • 根据startPos和endPos初始化页面ItemInfo:先从缓存里获取,没有就调用addNewItem方法,实际调用mAdapter.instantiateItem;
  • 将不需要的ItemInfo移除:mItems.remove(itemIndex),并调用mAdapter.destroyItem方法;
  • 设置LayoutParams参数(包括 position 和 widthFactor),根据 position 排序待绘制 View 列表mDrawingOrderedChildren,并重写getChildDrawingOrder方法;
  • 最后一步获取当前显示 View 的焦点:currView.requestFocus(View.FOCUS_FORWARD)。

04.3 dataSetChanged()

当调用 Adapter 的notifyDataSetChanged()时,会触发这个方法,重新计算当前页面的 position,并刷新页面:

  • 移除需要销毁的页面的ItemInfo对象,然后再调用populate方法刷新页面;
  • 循环mItems(每个 page 对应的ItemInfo对象),调用int newPos = mAdapter.getItemPosition方法;
  • 当newPos等于PagerAdapter.POSITION_UNCHANGED,表示当前页面不需要更新、不用销毁;当newPos等于PagerAdapter.POSITION_NONE,表示需要更新,移除 item 并调用mAdapter.destroyItem;
  • 循环完成后,最后计算出显示页面的newCurrItem,调用setCurrentItemInternal(newCurrItem, false, true)方法更新 UI(实际调用populate方法重新计算页面信息)。

04.4 scrollToItem 与 calculatePageOffsets

  • scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected):滑动到指定页面,内部会触发OnPageChangeListener;
  • calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo):主要用于计算每个页面对应ItemInfo的 offset 变量。该变量记录当前 View 在所有缓存 View(包含当前显示页)中的索引,用于布局时计算该 View 应该放在哪个位置。在populate方法中更新完页面数据后,会调用该方法计算所有页面的 offset。

从这套流程可以看出:ViewPager 完全通过mOffscreenPageLimit划定了"构造/销毁"的边界,Fragment 层面能做到的优化,就是延迟"真正耗时的初始化(发网络请求)"到用户可见的那一刻。

05. 懒加载出现问题:setUserVisibleHint 的时机陷阱

发现 Fragment 中有一个setUserVisibleHint(boolean isVisibleToUser)方法,这个方法就是告诉用户 UI 对用户是否可见,可以用来做懒加载初始化操作。但直接使用它有几个容易踩坑的地方:

  • 因为 ViewPager 会加载多个 Fragment,为了节省内存,会在 Fragment 不可见的某个时候调用onDestroyView()销毁用户界面,但 Fragment 实例还在。所以第一次加载可能没有问题,但是再次回到第一个 Fragment 再去加载时,会出现"UI 对用户可见但视图还没有初始化"的问题。

懒加载需要处理的几个问题:

  1. 预加载:虽然没有显示在界面上,但当前页面的上一页和下一页的 Fragment 已经执行了一个 Fragment 能显示在界面上的所有生命周期方法。我们想做到"跳转到该页时才真正构造数据视图和请求数据",那么可以使用一个占位视图——ViewStub。当真正跳转到该页时执行ViewStub.inflate()方法,加载真正的数据视图和请求数据。
  2. 视图保存:当某一页超出可视范围和预加载范围,它将会被销毁。FragmentStatePagerAdapter销毁整个 Fragment;可以自己保存该 Fragment,或使用FragmentPagerAdapter让 FragmentManager 保留 Fragment 的引用。虽然这样它的生命周期方法已经走完,只能手动保存 Fragment 根 View 的引用,当再次重新进入新的生命周期方法时返回原来的 View。
  3. 是否已经被用户所看到:其实 FragmentManager 本身并没有提供 Fragment 被用户看到的回调方法,而是在FragmentPagerAdapter和FragmentStatePagerAdapter中调用了Fragment.setUserVisibleHint(boolean)来表明 Fragment 是否已经被作为 primaryFragment。所以这个方法可以被认为是一个回调方法。

关于两种 Fragment 适配器在"销毁与恢复"上的差异,其源码实现见仓库中的 PagerAdapter详细介绍:

  • FragmentPagerAdapter.destroyItem内部调用mCurTransaction.detach((Fragment)object),只是 detach 而非 remove:销毁了 Fragment 的视图但没有移除 Fragment 本身,实例始终保留在内存中,重新可见时只需 attach 即可恢复视图。
  • FragmentStatePagerAdapter.destroyItem内部调用mCurTransaction.remove(fragment),同时通过mFragmentManager.saveFragmentInstanceState(fragment)缓存 Fragment 的状态,真正移除 Fragment;重新构造时会判断mSavedState中是否缓存了状态,若有则通过setInitialSavedState恢复。

这也对应了三种 Adapter 的缓存策略:

  • PagerAdapter:缓存三个页面,通过重写instantiateItem和destroyItem达到创建和销毁 View 的目的;
  • FragmentPagerAdapter:内部通过 FragmentManager 持久化每一个 Fragment,destroyItem 时只是 detach,并没有真正移除;
  • FragmentStatePagerAdapter:内部通过 FragmentManager 管理每一个 Fragment,destroyItem 时真正移除。

使用场景上,PagerAdapter适合视图比较简单的情形,FragmentPagerAdapter适合 Fragment 数量较少的页面(如 3、4 个 Tab 的主页),FragmentStatePagerAdapter适合条目数量特别多的场景(如仿抖音分页视频,见仿抖音滑动分页视频)。

06. 如何实现预加载机制:BaseLazyFragment 完整代码剖析

核心方法是 Fragment 中的setUserVisibleHint(),此方法会在onCreateView()之前执行;当 ViewPager 中 Fragment 改变可见状态时也会调用。当 Fragment 从可见到不可见、或者从不可见切换到可见时,都会调用此方法;使用getUserVisibleHint()可以返回 Fragment 当前是否可见的状态。

在BaseLazyFragment中需要在onActivityCreated()及setUserVisibleHint()方法中都调用一次lazyLoad()方法。如果仅仅在setUserVisibleHint()里调用lazyLoad(),当默认首页首先加载时会导致 ViewPager 的首页第一次展示时没有数据显示,切换一下才会有数据。原因在于:首页 Fragment 的setUserVisibleHint()在onActivityCreated()之前调用,此时isPrepared为 false,导致首页 Fragment 没能调用onLazyLoad()方法加载数据。

完整实现如下:

/** * <pre> * @author yangchong * time : 2017/7/22 * desc : 懒加载 * revise: 懒加载时机:onCreateView()方法执行完毕 + setUserVisibleHint()方法返回true * </pre> */ public abstract class BaseLazyFragment extends BaseFragment { /* * 预加载页面回调的生命周期流程: * setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()--> * onActivityCreate() --> onStart() --> onResume() */ /** * 懒加载过 */ protected boolean isLazyLoaded = false; /** * Fragment的View加载完毕的标记 */ private boolean isPrepared = false; /** * 第一步,改变isPrepared标记 * 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isPrepared标记为true,并调用lazyLoad()方法 */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isPrepared = true; //只有Fragment onCreateView好了 //另外这里调用一次lazyLoad() lazyLoad(); } /** * 第二步 * 此方法会在onCreateView()之前执行 * 当viewPager中fragment改变可见状态时也会调用 * 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法 * true表示当前页面可见,false表示不可见 */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); LogUtil.d("setUserVisibleHint---"+isVisibleToUser); //只有当fragment可见时,才进行加载数据 if (isVisibleToUser){ lazyLoad(); } } /** * 调用懒加载 * 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载 */ private void lazyLoad() { if (getUserVisibleHint() && isPrepared && !isLazyLoaded) { showFirstLoading(); onLazyLoad(); isLazyLoaded = true; } else { //当视图已经对用户不可见并且加载过数据,如果需要在切换到其他页面时停止加载数据,可以覆写此方法 if (isLazyLoaded) { stopLoad(); } } } /** * 视图销毁的时候讲Fragment是否初始化的状态变为false */ @Override public void onDestroyView() { super.onDestroyView(); isLazyLoaded = false; isPrepared = false; } /** * 第一次可见时,操作该方法,可以用于showLoading操作,注意这个是全局加载loading */ protected void showFirstLoading() { LogUtil.i("第一次可见时show全局loading"); } /** * 停止加载 * 当视图已经对用户不可见并且加载过数据,但是没有加载完,而只是加载loading。 * 如果需要在切换到其他页面时停止加载数据,可以覆写此方法。 * 存在问题,如何停止加载网络 */ protected void stopLoad(){ } /** * 第四步:定义抽象方法onLazyLoad(),具体加载数据的工作,交给子类去完成 */ @UiThread protected abstract void onLazyLoad(); }

06.1 onLazyLoad() 加载数据的三个条件

这套实现之所以能正确工作,依赖三个标记的联合判断:

  • getUserVisibleHint():返回 Fragment 是否可见状态,这是 Fragment 实现懒加载的关键,只有 Fragment 可见才会调用onLazyLoad()加载数据;
  • isPrepared:在系统调用onActivityCreated时设置为 true,此时onCreateView已调用完毕(一般我们在这个方法里执行findViewById等操作),确保onLazyLoad()方法不会报空指针异常;
  • isLazyLoaded:确保 ViewPager 来回切换时initData方法不会被重复调用,onLazyLoad在该 Fragment 的整个生命周期只调用一次,第一次调用onLazyLoad()方法后马上执行isLazyLoaded = true。

然后继承这个BaseLazyFragment实现onLazyLoad()方法即可,框架会自动控制在 Fragment 展现出来时才加载数据。

06.2 几个值得优化的细节

  • 停止加载:当视图已经对用户不可见且加载过数据时,如果需要在切换到其他页面时停止加载数据,可以覆写stopLoad方法。文档也坦诚指出其"存在问题,如何停止加载网络"——停止网络请求本身需要配合请求库的取消机制才能彻底实现。
  • 状态重置:视图销毁时把 Fragment 是否初始化的状态变为 false(onDestroyView中isLazyLoaded = false; isPrepared = false;),这样重新进入时才能再次懒加载。
  • 首次 Loading 与局部刷新分离:第一次可见时定义一个showFirstLoading方法,用于全局 Loading 加载操作。需要注意它和下拉刷新数据或局部刷新的 loading 不一样——可能有些开发 App 没有将 loading 分得这么细。

06.3 占位视图 ViewStub 的配合

文档中提到"使用一个占位视图,当真正跳转到该页时才真正构造数据视图和请求数据",这正是ViewStub的典型用法。ViewStub 是一个看不见、没有大小、不占布局位置的 View,专门用于布局懒加载;当调用inflate()或setVisibility(int)时布局才会被真正加载并替换掉 ViewStub。其构造方法中初始状态为setVisibility(GONE)、setWillNotDraw(true),因此不会参与绘制也不占空间。inflate()内部通过LayoutInflater加载目标布局、计算 ViewStub 在父布局中的 index,然后把 ViewStub 移除、把新布局插入到相同位置(详见仓库中的 ViewStub源码分析)。用它作为懒加载页面的占位视图,可以进一步减少首屏绘制负担。

07. 懒加载配合状态管理器:BaseStateFragment 完整实现

07.1 什么是状态管理器

一般在需要用户等待的场景,显示一个 Loading 动画可以让用户知道 App 正在加载数据,而不是程序卡死,从而给用户较好的使用体验。进一步地:

  • 当加载的数据为空时,显示一个数据为空的视图;
  • 数据加载失败时,显示加载失败对应的 UI 并支持点击重试,比白屏的用户体验更好;
  • 加载中、加载失败、空数据的 UI 风格,在 App 内所有页面中需要保持一致,也就是需要做到全局统一。

07.2 如何降低耦合性和入侵性

为了让 View 状态的切换和 Activity/Fragment 彻底分离开,需要把这些状态 View 都封装到一个管理类中,然后暴露几个方法来实现 View 之间的切换。由于不同项目需要的状态 View 不一样,管理类适合设计成 builder 模式来自由添加需要的状态 View。

一个低耦合、低入侵、易维护、易移植的状态管理方案大致应具备以下条件:

  • 可以运用在 Activity 或者 Fragment 中;
  • 不需要在布局中添加 LoadingView,而是统一管理不同状态视图,同时暴露对外设置自定义状态视图的方法,方便 UI 特定页面定制;
  • 支持设置自定义不同状态视图,即使在 BaseActivity 统一处理状态视图管理,也支持单个页面定制;
  • 加载视图时,异常和空页面能否用ViewStub代替,这样减少绘制,只有等到出现异常和空页面时,才将视图 inflate 出来;
  • 当页面出现网络异常页、空页面时,页面会有交互事件,这时候可以设置点击网络或点击重新加载等。

07.3 BaseStateFragment 完整代码

具体操作上,可以自由切换"内容、空数据、异常错误、加载、网络错误"等 5 种状态。父类BaseStateFragment直接暴露 5 种状态,方便子类统一管理状态切换,Fragment 的封装和 Activity 差不多:

/** * <pre> * @author yangchong * time : 2017/7/20 * desc : fragment的父类 * revise: 注意,该类具有懒加载 * </pre> */ public abstract class BaseStateFragment extends BaseLazyFragment { protected StateLayoutManager statusLayoutManager; private View view; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if(view==null){ view = inflater.inflate(R.layout.base_state_view, container , false); initStatusLayout(); initBaseView(view); } return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initView(view); initListener(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); } /** * 获取到子布局 * @param view view */ private void initBaseView(View view) { LinearLayout llStateView = view.findViewById(R.id.ll_state_view); llStateView.addView(statusLayoutManager.getRootLayout()); } /** * 初始化状态管理器相关操作 */ protected abstract void initStatusLayout(); /** * 初始化View的代码写在这个方法中 * @param view view */ public abstract void initView(View view); /** * 初始化监听器的代码写在这个方法中 */ public abstract void initListener(); /** * 第一次可见状态时,showLoading操作,注意下拉刷新操作时不要用该全局loading */ @Override protected void showFirstLoading() { super.showFirstLoading(); showLoading(); } /*protected void initStatusLayout() { statusLayoutManager = StateLayoutManager.newBuilder(activity) .contentView(R.layout.common_fragment_list) .emptyDataView(R.layout.view_custom_empty_data) .errorView(R.layout.view_custom_data_error) .loadingView(R.layout.view_custom_loading_data) .netWorkErrorView(R.layout.view_custom_network_error) .build(); }*/ /*---------------------------------下面是状态切换方法-----------------------------------------*/ /** * 加载成功 */ protected void showContent() { if (statusLayoutManager!=null){ statusLayoutManager.showContent(); } } /** * 加载无数据 */ protected void showEmptyData() { if (statusLayoutManager!=null){ statusLayoutManager.showEmptyData(); } } /** * 加载异常 */ protected void showError() { if (statusLayoutManager!=null){ statusLayoutManager.showError(); } } /** * 加载网络异常 */ protected void showNetWorkError() { if (statusLayoutManager!=null){ statusLayoutManager.showNetWorkError(); } } /** * 加载loading */ protected void showLoading() { if (statusLayoutManager!=null){ statusLayoutManager.showLoading(); } } }

07.4 如何切换状态

子类在拿到数据后,直接调用对应方法即可切换全局状态视图:

showContent(); showEmptyData(); showError(); showLoading(); showNetWorkError(); //或者这样操作也可以 statusLayoutManager.showLoading(); statusLayoutManager.showContent();

07.5 状态管理器的设计思路

状态管理器整体由三个核心部分组成:

  • StateFrameLayout:继承 FrameLayout 的自定义布局,主要作用是存放不同的状态视图,以及隐藏和展示视图的操作;
  • StateLayoutManager:状态管理器,主要是让开发者设置不同状态视图的 view,以及切换视图状态的操作。设计要点是:loading 和内容 View 在界面状态切换中一直需要加载显示,而空数据、异常、网络错误这 3 种状态只有在没数据或者网络异常的情况下才会加载显示,所以用ViewStub来加载它们可以提高性能(延迟 inflate、减少首帧绘制);
  • OnRetryListener:一个接口,主要作用是重试。比如加载失败了,点击视图需要重新刷新接口,就可以用到它;开发者也可以自己设置点击事件。

BaseStateFragment与BaseLazyFragment的组合是这套方案的精华:BaseStateFragment继承BaseLazyFragment,因此天然具备懒加载能力;同时覆写showFirstLoading()把"第一次可见"的时机与全局 Loading 的展示衔接起来(注释中特别提醒:下拉刷新操作时不要用该全局 loading)。这样"可见时才请求数据"与"请求期间的统一 Loading/失败/空态 UI"被有机整合在一个基类中,子类只需实现initStatusLayout、initView、initListener和onLazyLoad四个方法即可完成一个带完整状态管理的懒加载页面。

08. 总结:懒加载方案的完整链路

结合本文内容,一个生产可用的 ViewPager + Fragment 懒加载方案包含以下链路:

  1. 理解预加载的必然性:setOffscreenPageLimit(0)会被源码强制修正为 1,预加载无法通过 API 关闭,只能在 Fragment 层面做"可见才加载"的优化;
  2. 明确可见性回调:setUserVisibleHint()是FragmentPagerAdapter/FragmentStatePagerAdapter内部用于标识 primaryFragment 的回调,是懒加载的基石;
  3. 双重标记确保时机正确:isPrepared(View 已构建,防止空指针)与isLazyLoaded(防止重复加载)配合getUserVisibleHint(),并在onActivityCreated与setUserVisibleHint两处调用lazyLoad(),解决首页首次展示无数据的问题;
  4. 生命周期复位:在onDestroyView中重置isPrepared与isLazyLoaded,保证 Fragment 视图重建后能再次触发懒加载;
  5. 占位与状态管理:用ViewStub延迟加载真正的内容视图,用StateLayoutManager(builder 模式 + ViewStub 优化)统一管理 Loading、空数据、异常、网络错误等全局状态,二者都与懒加载时机天然衔接。

这套方案完整继承自仓库文档 android/08.复杂控件/03.ViewPager懒加载.md,其中的 PagerAdapter 协作细节、Fragment 适配器差异可参考 PagerAdapter详细介绍,ViewStub 底层原理见 ViewStub源码分析,真实业务落地案例可参考 仿抖音滑动分页视频(其中对视频页可见性判断、setOffscreenPageLimit(1)的取舍有更深入的实践讨论)。

  • 教程
  • 技术博客
  • 文档

【免费下载链接】YCBlogs

技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!

项目地址:https://gitcode.com/gh_mirrors/yc/YCBlogs
点击查看免费下载
上一篇:Feapder框架中的BatchSpider分布式批次爬虫详解
下一篇:Fetch GitHub Hosts终极指南:3分钟搞定GitHub访问加速

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 4:39:35

软件著作权介绍及申请流程

什么是软件著作权&#xff1f; 软件著作权是指软件的开发者或者其他权利人依据有关著作权法律的规定&#xff0c;对于软件作品所享有的各项专有权利。这种权利具备民事权利的共同特征&#xff0c;是一种民事权利。软件著作权从软件完成或部分完成之日起自动产生&#xff0c;无…

作者头像 李华
网站建设 2026/10/9 4:37:52

客户拜访总是记不全?我用这招把客户需求摸得透透的

做销售、做客户对接的朋友&#xff0c;多少都有过这样的经历&#xff1a;跟客户聊了快两小时&#xff0c;对方说了七八个需求点&#xff0c;当场听了全明白&#xff0c;回来一复盘——咦&#xff0c;第三个点到底是什么来着&#xff1f;那个报价细节是客户自己说的还是我记混了…

作者头像 李华
网站建设 2026/10/9 4:37:49

text-to-cad 实战:从自然语言到 STEP/STL 的几何生成流水线

1. 从一段文字到三维实体&#xff1a;text-to-cad 到底在解决什么问题第一次听到 "text-to-cad" 这个词&#xff0c;很多人会下意识地把它理解成"用嘴画图"——说一句话&#xff0c;软件自动帮你生成一张工程图纸。这个理解只对了一半。真正的 text-to-cad…

作者头像 李华
网站建设 2026/10/9 4:36:33

题解:洛谷 AT_abc425_b [ABC425B] Find Permutation 2

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/9 4:35:32

架构设计中的Protobuf实践:从序列化原理到跨语言通信的最佳方案

1. 为什么架构设计里要专门留一章给Protobuf1.1 从一个跨语言通信的痛点说起先分享一个我踩过的坑。有一段时间&#xff0c;我在负责一个内部系统的接口改造&#xff0c;上游是Java写的核心服务&#xff0c;下游是Python写的离线分析模块&#xff0c;中间还有几个Node.js的网关…

作者头像 李华