懶加載意思也就是當須要的時候纔會去加載。android
通常咱們都會在 onCreate() 或者 onCreateView() 裏去啓動一些數據加載操做,好比從本地加載或者從服務器加載。緩存
大部分狀況下,這樣並不會出現什麼問題,可是當你使用 ViewPager + Fragment的時候,問題就來了,這時就應該考慮是否須要實現懶加載了。服務器
ViewPager 爲了讓滑動的時候能夠有很好的用戶的體驗,也就是防止出現卡頓現象,所以它有一個緩存機制。ide
默認狀況下,ViewPager 會提早建立好當前 Fragment 旁的兩個Fragment。函數
舉個例子說, 也就是若是你當前顯示的是編號 3 的Fragment,那麼其實編號2和4的Fragment也已經建立好了,也就是說這3個Fragment都已經執行完 onAttach() -> onResume()
這之間的生命週期函數了。佈局
原本 Fragment 的 onResume()表示的是當前 Fragment 處於可見且可交互狀態,但因爲ViewPager的緩存機制,它已經失去了意義,也就是說咱們只是打開了 「3」 這個Fragment,但其實「2」 和 「4」這兩個Fragment的數據也都已經加載好了。ui
若是加載數據的操做都比較耗時或者都是相似圖片的佔用大量內存,這時就應該考慮想一想是否該實現懶加載。也就是, 當我打開哪一個Fragment的時候,它纔會去加載數據 。code
setUserVisibleHint(boolean isVisibleToUser)
是否可行?不少人推薦說把加載數據的操做放在這個函數裏,isVisibleToUser
表示當前 Fragment 是否可見。繼承
若是你只是須要數據的懶加載的話,這個固然是能夠的,但若是你還有如下的需求,那麼這種方式就不行了:生命週期
這邊再提一下,setUserVisibleHint()可能會在 Fragment 的生命週期以外被調用,也就是可能在view 建立前就被調用,也可能在 destroyView 後被調用,因此若是涉及到一些控件的操做的話,可能會報 null 異常,由於控件還沒初始化,或者已經摧毀了。
自定義了一個新的回調函數 onFragmentVisibleChange(boolean isVisible)
,能夠實現的效果有:
只有兩種狀況會觸發該函數:
能夠在該函數內進行控件的操做,不會報null異常
只有顯示的那個 Fragment和 離開的那個 Fragment 纔會觸發回調函數,這樣就能夠支持咱們在可見狀態變化時進行一些操做,由於不會有多餘的false觸發。
另外,由於ViewPager緩存機制,因此題主進行了 view 的複用,防止onCreateView()重複的建立view,其實也就是將view設置爲成員變量,建立view時先判斷是否爲null。
由於ViewPager裏對Fragment的回收和建立時,若是 Fragment已經建立過了,那麼只會調用 onCreateView() -> onDestroyView() 生命函數,onCreate()和onDestroy並不會觸發,因此關於變量的初始化和賦值操做能夠在onCreate()裏進行,這樣就能夠避免重複的操做。
具體代碼以下:
/** * * Viewpager + Fragment狀況下,fragment的生命週期因Viewpager的緩存機制而失去了具體意義 * 該抽象類自定義一個新的回調方法,當fragment可見狀態改變時會觸發的回調方法,介紹看下面 * * @see #onFragmentVisibleChange(boolean) */ public abstract class ViewPagerFragment extends Fragment { /** * rootView是否初始化標誌,防止回調函數在rootView爲空的時候觸發 */ private boolean hasCreateView; /** * 當前Fragment是否處於可見狀態標誌,防止因ViewPager的緩存機制而致使回調函數的觸發 */ private boolean isFragmentVisible; /** * onCreateView()裏返回的view,修飾爲protected,因此子類繼承該類時,在onCreateView裏必須對該變量進行初始化 */ protected View rootView; @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); Log.d(getTAG(), "setUserVisibleHint() -> isVisibleToUser: " + isVisibleToUser); if (rootView == null) { return; } hasCreateView = true; if (isVisibleToUser) { onFragmentVisibleChange(true); isFragmentVisible = true; return; } if (isFragmentVisible) { onFragmentVisibleChange(false); isFragmentVisible = false; } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); initVariable(); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (!hasCreateView && getUserVisibleHint()) { onFragmentVisibleChange(true); isFragmentVisible = true; } } private void initVariable() { hasCreateView = false; isFragmentVisible = false; } /************************************************************** * 自定義的回調方法,子類可根據需求重寫 *************************************************************/ /** * 當前fragment可見狀態發生變化時會回調該方法 * 若是當前fragment是第一次加載,等待onCreateView後纔會回調該方法,其它狀況回調時機跟 {@link #setUserVisibleHint(boolean)}一致 * 在該回調方法中你能夠作一些加載數據操做,甚至是控件的操做,由於配合fragment的view複用機制,你不用擔憂在對控件操做中會報 null 異常 * * @param isVisible true 不可見 -> 可見 * false 可見 -> 不可見 */ protected void onFragmentVisibleChange(boolean isVisible) { Log.w(getTAG(), "onFragmentVisibleChange -> isVisible: " + isVisible); } }
用法 :
新建你須要的Fragment類,繼承 ViewPagerFragment,在onCreateView()裏對rootView進行初始化 -> 重寫 onFragmentVisibleChange(),在這裏進行你須要的操做,好比數據加載,控制顯示等。
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if (rootView == null) { rootView = inflater.inflate(R.layout.fragment_android, container, false); } return rootView; } @Override protected void onFragmentVisibleChange(boolean isVisible) { super.onFragmentVisibleChange(isVisible); if (isVisible) { // do things when fragment is visible // if (ListUtils.isEmpty(mDataList) && !isRefreshing()) { // setRefresh(true); // loadServiceData(false); } else { // setRefresh(false); } } }
有些時候,咱們打開一個 Fragment 頁面時,但願它是在可見時纔去加載數據,也就是不要在後臺就開始加載數據,並且,咱們也但願加載數據的操做只是第一次打開該 Fragment 時才進行的操做,之後若是再從新打開該 Fragment 的話,就不要再重複的去加載數據了。
具體點說,Fragment 和 ViewPager 一塊兒用時,因爲 ViewPager 的緩存機制,在打開一個 Fragment 時,它旁邊的幾個Fragment 其實也已經被建立了,若是咱們是在 Fragment 的 onCreat() 或者 onCreateView() 裏去跟服務器交互,下載界面數據,那麼這時這些已經被建立的Fragment,就都會出如今後臺下載數據的狀況了。因此咱們一般須要在 setUserVisibleHint() 裏去判斷當前 Fragment 是否可見,可見時再去下載數據,可是這樣仍是會出現一個問題,就是每次可見時都會重複去下載數據,咱們但願的是隻有第一次可見時才須要去下載,那麼就還須要再作一些判斷。這就是要封裝個基類來作這些事了,具體代碼見後面。
即便咱們在 setUserVisibleHint() 作了不少判斷,實現了可見時加載而且只有第一次可見時才加載,可能仍是會遇到其餘問題。
好比說,我下載完數據就直接須要對 ui 進行操做,將數據展現出來,但有時卻報了 ui 控件 null 異常,這是由於 setUserVisibleHint() 有可能在 onCreateView() 建立 view 以前調用,並且數據加載時間很短,這就可能出現 null 異常了,那麼咱們還須要再去作些判斷,保證在數據下載完後 ui 控件已經建立完成。
除了懶加載,只加載一次的需求外,可能咱們還須要每次 Fragment 的打開或關閉時顯示數據加載進度。對吧,咱們打開一個 Fragment 時,若是數據還沒下載完,那麼應該給個下載進度或者加載框提示,若是這個時候打開了新的 Fragment 頁面,而後又從新返回時,若是數據還沒加載完,那麼也還應該繼續給提示,對吧。這就須要有個 Fragment 可見與不可見時觸發的回調方法,而且該方法還得保證是在 view 建立完後才觸發的,這樣才能支持對ui 進行操做。
/* * * Fragment基類,封裝了懶加載的實現 * * 一、Viewpager + Fragment狀況下,fragment的生命週期因Viewpager的緩存機制而失去 了具體意義 * 該抽象類自定義新的回調方法,當fragment可見狀態改變時會觸發的回調方法, 和 Fragment 第一次可見時會回調的方法 * * @see #onFragmentVisibleChange(boolean) * @see #onFragmentFirstVisible() */ public abstract class BaseFragment extends Fragment { private static final String TAG = BaseFragment.class.getSimpleName(); private boolean isFragmentVisible; private boolean isReuseView; private boolean isFirstVisible; private View rootView; //setUserVisibleHint()在Fragment建立時會先被調用一次, 傳入isVisibleToUser = false //若是當前Fragment可見,那麼setUserVisibleHint()會再次被調用一次, 傳入isVisibleToUser = true //若是Fragment從可見->不可見,那麼setUserVisibleHint()也會被調用, 傳入isVisibleToUser = false //總結:setUserVisibleHint()除了Fragment的可見狀態發生變化時會被回調外, 在new Fragment()時也會被回調 //若是咱們須要在 Fragment 可見與不可見時乾點事, 直接使用用這個方法的話就會有多餘的回調了,那麼就須要從新封裝一個 @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); //setUserVisibleHint()有可能在fragment的生命週期外被調用 if (rootView == null) { return; } if (isFirstVisible && isVisibleToUser) { onFragmentFirstVisible(); isFirstVisible = false; } if (isVisibleToUser) { onFragmentVisibleChange(true); isFragmentVisible = true; return; } if (isFragmentVisible) { isFragmentVisible = false; onFragmentVisibleChange(false); } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); initVariable(); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { //若是setUserVisibleHint()在rootView建立前調用時,那麼 //就等到rootView建立完後纔回調onFragmentVisibleChange(true) //保證onFragmentVisibleChange()的回調發生在rootView建立完成以後,以便支持ui操做 if (rootView == null) { rootView = view; if (getUserVisibleHint()) { if (isFirstVisible) { onFragmentFirstVisible(); isFirstVisible = false; } onFragmentVisibleChange(true); isFragmentVisible = true; } } super.onViewCreated(isReuseView ? rootView : view, savedInstanceState); } @Override public void onDestroyView() { super.onDestroyView(); } @Override public void onDestroy() { super.onDestroy(); initVariable(); } private void initVariable() { isFirstVisible = true; isFragmentVisible = false; rootView = null; isReuseView = true; } /** * 設置是否使用 view 的複用,默認開啓 * view 的複用是指,ViewPager 在銷燬和重建 Fragment 時會不斷調用 onCreateView() -> onDestroyView() * 之間的生命函數,這樣可能會出現重複建立 view 的狀況,致使界面上顯示多個相同的 Fragment * view 的複用其實就是指保存第一次建立的 view,後面再 onCreateView() 時直接返回第一次建立的 view * * @param isReuse */ protected void reuseView(boolean isReuse) { isReuseView = isReuse; } /** * 去除setUserVisibleHint()多餘的回調場景,保證只有當fragment可見狀態發生變化時纔回調 * 回調時機在view建立完後,因此支持ui操做,解決在setUserVisibleHint()裏進行ui操做有可能報null異常的問題 * * 可在該回調方法裏進行一些ui顯示與隱藏,好比加載框的顯示和隱藏 * * @param isVisible true 不可見 -> 可見 * false 可見 -> 不可見 */ protected void onFragmentVisibleChange(boolean isVisible) { } /** * 在fragment首次可見時回調,可在這裏進行加載數據,保證只在第一次打開Fragment時纔會加載數據, * 這樣就能夠防止每次進入都重複加載數據 * 該方法會在 onFragmentVisibleChange() 以前調用,因此第一次打開時,能夠用一個全局變量表示數據下載狀態, * 而後在該方法內將狀態設置爲下載狀態,接着去執行下載的任務 * 最後在 onFragmentVisibleChange() 里根據數據下載狀態來控制下載進度ui控件的顯示與隱藏 */ protected void onFragmentFirstVisible() { } protected boolean isFragmentVisible() { return isFragmentVisible; } }
使用很簡單,新建你須要的 Fragment 類繼承自該 BaseFragment,而後重寫兩個回調方法,根據你的須要在回調方法裏進行相應的操做好比下載數據等便可。 例如:
public class CategoryFragment extends BaseFragment { private static final String TAG = CategoryFragment.class.getSimpleName(); @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_category, container, false); initView(view); return view; } @Override protected void onFragmentVisibleChange(boolean isVisible) { if (isVisible) { //更新界面數據,若是數據還在下載中,就顯示加載框 notifyDataSetChanged(); if (mRefreshState == STATE_REFRESHING) { mRefreshListener.onRefreshing(); } } else { //關閉加載框 mRefreshListener.onRefreshFinish(); } } @Override protected void onFragmentFirstVisible() { //去服務器下載數據 mRefreshState = STATE_REFRESHING; mCategoryController.loadBaseData(); } }
注意事項 :
若是想要讓 fragment 的佈局複用成功,須要重寫 viewpager 的適配器裏的 destroyItem() 方法,將 super 去掉,也就是不銷燬 view。
若是出現切換回來或不相鄰的 Tab 切換時致使空白界面的問題,解決方法:在 onCreateView中複用佈局 + ViewPager 的適配器中複寫 destroyItem() 方法去掉 super。