ViewPager2 延遲加載數據
ViewPager 實現預加載的方案
背景
現在項目采用的viewpager + Tablayout的聯合使用, 為了優化頁面加載流暢性的問題,希望采取的懶加載策略,但是因為使用的是viewpager需要通過Fragment的setUserVisibleHint的回調來得知當前Fragment是否可見。
可見下方示例代碼
| activity代碼
final ViewPager viewpager = findViewById(R.id.vp_content);
final List<BlankFragmentV1> fragments = new ArrayList<>();
for (int i = 0; i < 20; i++) {
fragments.add(new BlankFragmentV1(i));
}
viewpager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return String.valueOf(position);
}
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
});
tabLayout.setupWithViewPager(viewpager);
// 至少為1, 預預載的Fragment一定會走onResume方法,ViewPager對offscreenPageLimit的設置並不友好
// 只能依賴setUserVisibleHint判斷當前Fragment是否處於可見狀態
viewpager.setOffscreenPageLimit(1);
| Fragment代碼
public class BlankFragmentV1 extends Fragment {
// 數據是否加載
private boolean isDataLoad;
// 視圖是否已構建
private boolean isPrepared;
private static final String TAG = "BlankFragmentV1";
private int position;
public BlankFragmentV1(int position) {
this.position = position;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.i(TAG, "onCreateView: " + position);
loadData();
isDataLoad = true;
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.i(TAG, "onViewCreated: " + position);
}
@Override
public void onStart() {
super.onStart();
Log.i(TAG, "onStart: " + position);
}
@Override
public void onResume() {
super.onResume();
Log.i(TAG, "onResume: " + position);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy: " + position);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
if (!isDataLoad && isPrepared) {
loadData();
isDataLoad = true;
}
}
Log.i(TAG, "setUserVisibleHint: " + position + " " + isVisibleToUser);
}
@Override
public void onDestroyView() {
super.onDestroyView();
isDataLoad = false;
isPrepared = false;
}
public void loadData() {
Log.i(TAG, "lazy load" + position);
}
}

上述代碼示例中的問題就是viewPager offscreenPageLimit的設置只能控制離屏加載的fragment個數,但其實預加載的fragment還是會走onResume的生命周期,所以沒法通過onResume來判斷Fragment是否可見,只能通過setUserVisibleHint來判斷是否View可見。但也不能完全依賴setUserVisibleHint來加載數據,因為默認的第一個Fragment調用setUserVisibleHint時,因為其onViewCreated還沒走,所以View還沒創建。
總上所述:ViewPager來實現延遲加載數據並不方便,限制很多,實現起來也不優雅,且setUserVisibleHint這個方法本就已經是要廢棄的方法,不應該繼續使用了
ViewPager2 實現預加載的方案
ViewPager2可以理解為google后來推出的ViewPager增強版,增加了很多新的功能,如:
- 垂直方向支持
- 支持diffUtil
- 支持RTL
- ...
| activity代碼
final List<BlankFragmentV2> fragments = new ArrayList<>();
for (int i = 0; i < 20; i++) {
fragments.add(new BlankFragmentV2(i));
}
viewpager.setAdapter(new FragmentStateAdapter(this) {
@NonNull
@Override
public Fragment createFragment(int position) {
BlankFragmentV2 fragment = fragments.get(position);
return fragment;
}
@Override
public int getItemCount() {
return fragments.size();
}
});
viewpager.setOffscreenPageLimit(fragments.size());
new TabLayoutMediator(tabLayout, viewpager, true, (tab, position) -> {
tab.setText("fragment"+position);
tab.setIcon(R.drawable.icon);
}).attach();
// 禁止用戶左右滑動
// viewpager.setUserInputEnabled(false);
viewpager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
Log.i(TAG, "onPageSelected: " + Objects.requireNonNull(tabLayout.getTabAt(position)).getText());
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
viewpager.setOffscreenPageLimit(1);
| Fragment代碼
private boolean isFirstLoad = true;
private static final String TAG = "BlankFragmentV2";
private int position;
public BlankFragmentV2(int position) {
this.position = position;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.i(TAG, "onCreateView: " + position);
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.i(TAG, "onViewCreated: " + position);
}
@Override
public void onStart() {
super.onStart();
Log.i(TAG, "onStart: " + position);
}
@Override
public void onResume() {
super.onResume();
Log.i(TAG, "onResume: " + position);
if (isFirstLoad) {
isFirstLoad = false;
loadData();
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy: " + position);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.i(TAG, "setUserVisibleHint: " + position + " " + isVisibleToUser);
}
private void loadData() {
Log.i(TAG, "lazy load" + position);
}

上述代碼中可以看到通過使用ViewPager2我們完全可以依賴於OnResume來進行進行延遲加載數據,相比於ViewPager而言簡易得多,而setOffscreenPageLimit中設置的離屏加載的值會幫助我們需要預載多少個相離的界面和需要銷毀的界面,如下圖所示

總結
ViewPager2在各方面的能力都會比ViewPager強,其配套的FragmentStateAdapter在遇到預加載時,只會創建Fragment對象,不會把Fragment真正地加入布局中,自帶懶加載效果。
PS:需要注意是FragmentStateAdapter不會一直保持Fragment實例,在被destroy后,需要做好Fragment重建后回復數據的准備,這點可以結合ViewModel來進行配合使用。
