深入一點 讓細節幫你和Fragment更熟絡

有一段時間沒有寫博客了,作為2017年的第一篇,初衷起始于前段時間一個接觸安卓開發還不算太長時間的朋友聊到的一個問題:

“假設,想要對一個Fragment每次在隱藏/顯示之間做狀態切換時進行監聽, 從而在這個時候去完成一些操作,應該怎么去實現呢?"

相信大家聽到這類問題第一反應都會覺得是很容易的。而又經過一番討論過后,發現他的問題場景相對來說比較特殊一點的是:

其想要監聽的Fragment是嵌套在另一層Fragment內的子Fragment。這就更有趣了一點,當然了,這個需求場景同樣也不會太難實現。

既然這樣還寫什么博客呢?哈哈。問題本身雖然不算難解決,但個人發現在對其解決的過程中,其實能涉及到不少對于Fragment較實用的小細節。

那么,自己也可以剛好借此機會,重新更加深入的回顧、整理和總結一下關于Fragment的一些使用細節和技巧。何樂而不為呢?特此記錄。


<h1>問題場景還原 </h1>

場景還原

如上圖所示,這一圖例基本上包含了現在大多數主流APP常用的一種UI設計模式。有底部導航,有ViewPager,有側拉菜單等等。

其實對于這種UI模式,有一個非常直觀的印象就是“碎片化”。那么,對應到Android中,用Fragment去實現這種設計就再合適不過了。

那么,我們也就可以看到:在這里,用戶的一系列操作就會涉及到大量的Fragment的隱藏和顯示的狀態切換工作。

從而提歸正傳,我們試圖在這一圖例中去模擬的還原一下之前說到的那個問題。首先,我們來分解一下這個用例中的UI設計:

  • 首先自然是主界面,主界面是一個Activity。Ac中有一個底部導航,分為三頁,三頁自然分別對應了三個Fragment。
  • 第二頁和第三頁的Fragment界面,我沒有去添加額外的設計了,所以十分一目了然,故不加贅述。
  • 重點在第一頁,可以看到這一頁中有一個側滑菜單,側滑菜單里的選項又對應了另外的Fragment界面。
  • 那么,很顯然的,側滑菜單里的界面,就是嵌套在底部導航的第一頁Fragment里的另一層Fragment了。
  • 最后,我們可以看到嵌套在側滑菜單里的第一個子Fragment,它里面是一個ViewPager,于是又涉及到兩個新的子Fragment。

OK,到了這里,有了這一番UI分解,我們有了一個大概的了解。現在我們借助一個實際的常用功能更好的還原我們之前說到的那個問題。

假設在底部導航的“第三頁”界面中,有一個功能叫做“清除緩存”。那么,使用這一功能就意味著:其它界面當中原先緩存的數據將被清除。

也就意味著,當用戶再次切換到另外的界面中時(Fragment由隱藏切換到顯示),就需要清除該界面原本的內容,重新獲取最新的內容顯示。

OK,現在已經回到了我們最初說到的話題了。那么,接著就讓我們以這個例子切入,由易到難的看一下:

在常見的各種情況下,應該如何監聽Fragment的顯示狀態切換。而在這其中,又可以注意哪些關于Fragment比較實用的小細節。


<h1>replace與hide/show</h1>

在上一節的圖例中,我們說到主界面Activity中有一個底部導航欄,分別對應著三個功能界面(即三個Fragment)。

顯然,我們肯定有兩種方式來控制這種Fragment的導航切換,即使用replace進行切換,或者通過hide/show來控制切換。

以我們的圖例來說,假設我們想要從“第一頁”切換到“第二頁”,那么我們可以這樣做(replace):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_container, new SecondFragment());
ft.commit();

當然也可以這樣做(hide/show):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.hide(new FirstFragment())
  .show(new SecondFragment())
  .commit();

但這里就注意了:如果我們是剛開始接觸Fragment,上面的代碼看上去似乎沒問題,但實際肯定是不能這樣去使用hide/show的。

因為就像上述代碼表述的一樣,我們一定要留意到“new ”,它看上去就像在說:每次隱藏和顯示的都是一個全新的Fragment對象。

而事實上也的確如此。所以,這也就意味著:如果這么搞,我們肯定是沒辦法正確的控制fragment的切換顯示的。

那么,我們應該怎么去完成這種需求呢?實際可以提供兩種方式,第一種就是為Fragment添加“單例”。

以我們圖例中顯示的來說,當進入主界面后,優先顯示的是“第一頁的界面”,所以我們可以先讓它顯示:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.add(R.id.fragment_container, FirstFragment.getInstance());
ft.add(R.id.fragment_container, SecondFragment.getInstance());
ft.add(R.id.fragment_container, ThirdFragment.getInstance());
ft.hide(SecondFragment.getInstance());
ft.hide(ThirdFragment.getInstance());

ft.commit();

于是在此之后,由“第一頁”的Fragment切換到“第二頁”的工作,則可以通過類似下面的代碼來實現:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
                
ft.hide(FirstFragment.getInstance())
  .show(SecondFragment.getInstance())
  .commit();

你肯定注意到,這里我選擇先將會使用到的Fragment對象都添加到內存中去,讓暫時不需要顯示的碎片先hide。

需要額外說明的是: 這種做法本身其實也是可行的,但在以上的用例里我確實是省方便,而選擇了這樣的做法。而實際上來說:

個人覺得,如果我們其實并沒有讓暫時不需要顯示的Fragment進行“預加載”的需求的話。那么對應來說:

選擇在真正需要切換Fragment顯示的時候,再將要顯示的Fragment對象進行add,然后控制它們hide和show是更好的做法。

而之所以說這樣做更好的原因是什么呢?我們稍微放一放,在之后不久的分析里我們就可以看到。

上述的“單例”這種方式能完成我們的需求嗎?當然是可以的。但讓人滿意嗎?似乎總覺得有些別扭。

的確如此,這種方式更像是用Java的方式去解決問題,而非使用Android的方式來解決問題。所以,我們接著看第二種方式,即使用FragmentManager自身來管理我們的Fragment對象。

首先,我們要知道,通過FragmentManager開啟事務來動態添加Fragment對象的時候,也是可以為Fragment設置標識的。

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

FirstFragment first = new FirstFragment();
SecondFragment second = new SecondFragment();
ThirdFragment third = new ThirdFragment();

ft.add(R.id.fragment_container,first, "Tab0");
ft.add(R.id.fragment_container,second,"Tab1");
ft.add(R.id.fragment_container,third,"Tab2");

ft.hide(second);
ft.hide(third);

ft.commit();

上述代碼中的“Tab0”這種東西就是我們為Fragment對象設置的標識(Tag),其好處就在于我們之后能夠非常方便的控制不同Fragment的切換。

//這里是導航切換的回調
public void onTabSelected(int position) {
    if(position != currentFragmentIndex)
    {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();

        ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
          .show(fm.findFragmentByTag("Tab"+position))
          .commit();
                    
         currentFragmentIndex = position;
     }
 }

就像這里做的,其實FragmentManager本身就有一個List來存放我們add的Fragment對象。這意味著:

我們通過設置的Tag,可以直接復用FragmentManager中已經add過的Fragment對象,而無需所謂的“單例”。

在上述代碼中:自定義的currentFragmentIndex用于記錄當前所在的導航頁索引,position則意味著要切換顯示的界面的索引。

那么,再配合上我們對Fragment對象進行add的時候設置的Tag,便能非常方便簡潔的實現Fragment的切換顯示了。

回到之前說的,我們也可以選擇不一次性將三個fragment進行add,而是做的更極致一點。而原理依然非常簡單:

            public void onTabSelected(int position) {
                if (position != currentFragmentIndex) {
                    FragmentManager fm = getSupportFragmentManager();
                    FragmentTransaction ft = fm.beginTransaction();

                    Fragment targetFragment = fm.findFragmentByTag("Tab" + position);
                    if (targetFragment == null) {
                        switch (position) {
                            case 1:
                                targetFragment = new SecondFragment();
                                break;
                            case 2:
                                targetFragment = new ThirdFragment();
                                break;
                        }
                         ft.add(R.id.fragment_container,targetFragment,"Tab"+position);
                    }

                    ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
                      .show(targetFragment)
                      .commit();
                    
                    currentFragmentIndex = position;
                }
      }

與之前不同的就是:現在我們最初只add需要顯示的FirstFragment。然后在切換Fragment顯示的時候,首先通過findFragmentByTag查找對應的Fragment對象是否已經被add進了內存,如果沒有則新建該對象,并進行add。而如果已經存在,則可以直接進行復用,并控制隱藏與顯示的切換了。

好了,到這里我們看到通過這兩種方式都可以實現切換Fragment顯示的需求。那么,其差別在哪里呢?我們可以簡單的到源碼中找下答案。


<h2>從源碼看replace與add的異同</h2>

首先,我們可以明確一點的是:無論是使用到replace還是使用hide/show,前提都需要確保將相關的Fragment對象放入FragmentManager。

那么,在之前的用例描述中:對于hide/show的使用來說,我們知道首先是通過add的方式來進行的。就像如下代碼所做的這樣:

ft.add(R.id.fragment_container, first, "Tab0");

那么,對于replace來說又是如何呢?其實我們可以打開replace方法的源碼看一看說明:

以上截圖是源碼中對于replace方法的注釋說明,我們閱讀一下發現:簡單的來說,它似乎是在告訴我們,調用replace方法,效果基本就等同于:

先調用remove方法刪除掉指定containerViewId中當前所有已添加(add)的fragment對象,然后再通過add方法將replace傳入的對象添加。

看到這里,我們難免在想,這么說其實replace和add在本質上來說,是很相似的,其實這樣說也沒錯。通過以下代碼可以驗證上面的結論。

Log.d(TAG,getSupportFragmentManager().getFragments().size()+"");

通過以上代碼可以獲取當前FragmentManager中存放的有效的fragment對象的數量,那么對于我們上面說到的用例中:

  • 當使用replace控制fragment的顯示時,會發現獲取到的碎片數始終是1。因為每次replace時,都會將之前存在的fragment對象remove掉。
  • 當使用hide/show控制時,獲取到的碎片數將與我們進行add的數量相同。比如之前我們在首頁add了3個fragment,獲取到的數量就是3。

那么,fragment內部究竟是怎樣的機制,才會造成這樣的結果呢?回顧一下:
其實我們在管理fragment的時候,始終在和兩個東西打交道,那就是:FragmentManager與FragmentTransaction。

FragmentManager其實是一個抽象類,它的具體實現是FragmentManagerImpl,其內部有這樣的東西:

ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
//......

我們前面說到FragmentManager自身就有一個集合來存放fragment對象,其實就是上面這樣的東西。

FragmentTransaction其實也是一個抽象類,通過FragmentManagerImpl其中的代碼,我們可以知道其具體實現:

是的,FragmentTransaction的具體實現其實是一個叫做BackStackRecord的類。由此為基礎,我們就可以看看add和replace究竟做了什么樣的工作。


可以看到add和replace很重要的一個的區別在于某種行為標識:“OP_ADD”與“OP_REPLACE”。

但它們二者最終都是來到一個叫做doAddOp的方法,截取這個方法內目的性最強的部分代碼如下:

可以看到這里做的其實就是創建一個Op類型的對象,然后執行addOp方法。那么,我們首先看看Op是個什么東西?

好吧,連我數據結構這么渣的,也看出這就是一個鏈表結構的東東啦。其實不難理解,因為我們在正式commit事務之前:

其實可以執行一系列的add,replace,hide,remove等等的操作,所以肯定是需要一個類似鏈表這樣的數據結構來更好的記錄這些信息的。

那么,至于addOp這個方法就不難想象了,其實就是通過修改鏈表節點信息來記錄所做的類似add這樣的操作。節省篇幅,就不貼源碼截圖了。

但問題是,到現在我們還沒有和之前說到的FragmentManager產生聯系。這是沒錯的,因為真正和FM產生聯系,自然是在commit之后。

這里由于能力和篇幅有限,就不會做詳細的逐步分析了,總之我們明白一點:BackStackRecord本身實現了Runnable接口。接著,在commit之后的一系列相關調用之后,最終則會進入到BackStackRecord的run()方法開始執行。

那么,現在我們截取run()方法內我們關心的部分代碼來看看:

              switch (op.cmd) {
                case OP_ADD: {
                    Fragment f = op.fragment;
                    f.mNextAnim = enterAnim;
                    mManager.addFragment(f, false);
                } break;
                case OP_REPLACE: {
                    Fragment f = op.fragment;
                    int containerId = f.mContainerId;
                    if (mManager.mAdded != null) {
                        for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
                            Fragment old = mManager.mAdded.get(i);
                            if (FragmentManagerImpl.DEBUG) Log.v(TAG,
                                    "OP_REPLACE: adding=" + f + " old=" + old);
                            if (old.mContainerId == containerId) {
                                if (old == f) {
                                    op.fragment = f = null;
                                } else {
                                    if (op.removed == null) {
                                        op.removed = new ArrayList<Fragment>();
                                    }
                                    op.removed.add(old);
                                    old.mNextAnim = exitAnim;
                                    if (mAddToBackStack) {
                                        old.mBackStackNesting += 1;
                                        if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
                                                + old + " to " + old.mBackStackNesting);
                                    }
                                    mManager.removeFragment(old, transition, transitionStyle);
                                }
                            }
                        }
                    }
                    if (f != null) {
                        f.mNextAnim = enterAnim;
                        mManager.addFragment(f, false);
                    }
                } break;

首先是OP_ADD,可以看到處理的代碼非常簡單,關鍵的就是那句:

mManager.addFragment(f, false);

我們回到FragmentManagerImpl來中看看這句代碼究竟做了什么:

    public void addFragment(Fragment fragment, boolean moveToStateNow) {
        if (mAdded == null) {
            mAdded = new ArrayList<Fragment>();
        }
        if (DEBUG) Log.v(TAG, "add: " + fragment);
        makeActive(fragment);
        if (!fragment.mDetached) {
            if (mAdded.contains(fragment)) {
                throw new IllegalStateException("Fragment already added: " + fragment);
            }
            mAdded.add(fragment);
            fragment.mAdded = true;
            fragment.mRemoving = false;
            if (fragment.mHasMenu && fragment.mMenuVisible) {
                mNeedMenuInvalidate = true;
            }
            if (moveToStateNow) {
                moveToState(fragment);
            }
        }
    }

這個方法其實也不復雜,關鍵的信息就是將fragment對象加入mAdded集合中,而makeActive則會將其加入到mActive集合。
(之前說到的通過getFragmentManager.getFragments這句代碼返回的其實就是mActive這個集合)

而最后的moveToState就是真正關鍵的改變fragment對象狀態的操作了,這個方法比較復雜,這里不深入分析了。

現在我們回頭繼續看run方法中OPP_REPLACE執行的操作如何,事實上有了之前的基礎,我們不難發現:
replace的不同之處就在于,會先把mManager中mAdded集合內相同contanierViewID的fragment對象遍歷出來刪除掉:

mManager.removeFragment(old, transition, transitionStyle);

然后最后就像我們之前知道的那樣,其實依舊是把fragment對象添加到mManager的集合中去:

if (f != null) {
   f.mNextAnim = enterAnim;
   mManager.addFragment(f, false);
  }

現在我們應該明白,replace和add為什么會造成在mManger中存放的數量不同以及源碼中replace方法的注釋說明的原因了。但繼續延伸吧。

回憶一下:我們知道FragmentTransaction實際上還有一個功能叫做addToBackStack(),故名思議,就是說將Fragment對象加入返回棧。

其實這個方法的實際操作,在之前我們分析過的源碼中也可以得知。在BackStackRecord的run方法中,對于OPP_REPLACE操作有如下代碼:

if(mAddToBackStack)就是指使用了addToBackStack方法的情況,這時執行的一個操作是將fragment對象的mBackStackNesting變量自增。
隨后緊接著的操作就是通過mManager去removeFragment,重點就在這里,讓我們看看removeFragment方法的源碼:

注意final boolean inactive = !fragment.isInBackStack();這行代碼,其具體實現為:

也就是說,由于addToBackStack的存在,會導致inactive的獲取結果為false。所以這時根本不會真正去執行if語句塊里真正刪除fragment對象的操作。

這顯然是符合邏輯的,將fragment對象加入返回棧,意味著我之后還可能會使用到該對象,你自然不能像之前一樣把它們清除掉。

當然,我們還可以自己在addToBackStack使用之后,再通過fragmentManager.getFragments.size()去驗證一下獲取到的fragment對象的數量變化。


<h2>生命周期的不同</h2>

我們現在已經知道當通過add,replace,remove等操作時,最終會通過moveToState去改變對應fragment對象的狀態。

那么,hide/show又是如何呢?于是我們又回到了BackStackRecord的run方法當中尋覓答案:

那我們就以hideFragment作為例子,看看這時mManager究竟是做的什么工作呢?

可以看到這時實際上就告別了moveToState,其本質在于通過改變fragment對象的mView的可視性來控制顯示,并回調onHiddenChanged。

以上的分析的目的為何呢?其實就是為了證明,通過replace和hide/show兩種方式,最大的區別就在于:二者的生命周期變化相去甚遠。

我們還是可以自己去驗證這點。最簡單的方式是:寫一個基類的BaseFragment,然后為所有生命周期回調添加日志打印,就類似于如下這樣:

public class BaseFragment extends Fragment{

    protected String clazzName = this.getClass().getSimpleName() +" ==> ";

    @Override
    public void onAttach(Context context) {
        Log.d(clazzName,"onAttach");
        super.onAttach(context);
    }
    
    //......
}

那么,當我們使用replace進行切換顯示的時候,會發現其生命周期的路線類似于下面這樣:

然后我們切換到“hide/show”來觀察生命周期的變化,發現其回調如下所示:

而使用replace時,是否使用addToBackStack的另一個區別也是生命周期上的不同。

沒有使用addToBackStack的時候,被切換的對象和切換進來的對象的生命周期分別為:

  • onPause → onStop → onDestroyView → onDestroy → onDeatch
  • onAttach → onCreate → onCreateView → onViewCreated → onActivityCreated → onStart → onResume

而當使用了addToBackStack后,被切換的對象的生命周期變化則成了:

  • onPause → onStop → onDestroyView

而切換進行的對象,如果是首次進行切換,則與之前無異。反之,如果已經存在于返回棧內,生命周期變化則成了:

  • onCreateView → onViewCreated → onActivityCreated → onStart → onResume

現在我們回到之前說到的一個問題,為什么說不需要在最初就把潛在的幾個Fragment一股腦進行add,有了之前分析的基礎,我們知道:

當我們把fragment進行add過后,最終會執行到moveToState方法進行狀態設置,那對應到我們之前的例子中來說的話:

就代表著我們最初添加的三個fragment對象,都會經歷onAttach → onCreate → ...... → onResume這一初始化生命周期。

這意味著添加的三個fragment對象中,“第二頁”與“第三頁”雖然目前不用顯示,但系統需要耗費時間去完成它們的初始化周期。

這顯然在一定程度上會影響效率。當然,具體要怎么使用其實還是看實際的需求哪種更合適。我們只要明白其中的細節就可以了。

現在,我們思考一個問題,對于我們本文中的圖例應用來說,究竟使用replace還是hide/show更適合呢?其實有了之前的基礎,我們知道:

ft.add(R.id.fragment_container,first, "Tab0");

這行代碼的效果,其實完全可以用這樣使用replace來轉換:

 ft.replace(R.id.fragment_container,first,"Tab0").addToBackStack(null);

但是,我們前面也說到了,最大的區別就在于二者生命周期變化的不同。再分析一下:

  • 首先,如果我們使用的是replace切換fragment的顯示,那顯然我們需要使用addToBackStack。否則就無需談什么監聽由隱藏到顯示了,因為單獨replace每次都意味著切換進的是一個全新的fragment對象。
  • 如果使用replace+addToBackStack,那么被切換的fragment對象(即隱藏的對象)與切換進的fragment對象(即顯示的對象)的生命周期變化路線我們都已經清楚了,這時就有點類似監聽Activity了。
  • 使用hide/show來控制顯示切換,顯然是最簡單的,監聽onHiddenChange回調,實現自己的目的就可以了。

上述的第2、3種方式都能實現目的,但replace最大的缺陷就在于每次切換都會進入onCreateView到onResume這一周期。

這其中總會涉及到我們在fragment這些生命周期中做出的一些例如數據初始化的操作等,顯然這樣控制起來是非常麻煩的(數據重復加載等)。

所以,綜合比較之下,顯然hide/show才是最合適的方式。而對于replace來說,最適合的顯然就還是那種比較典型的例子:
比如pad上的新聞應用,左邊是新聞列表,右邊為新聞的詳細內容,這時右邊的fragment用replace來切換新聞內容顯示就是最合適的。

那么回到我們本文之前圖例里的演示應用,那么比如在“第三頁”清除緩存后,切換到了“第二頁”。
這時使用hide/show切換fragment顯示,然后通過onHiddenChange完成監聽就非常簡單了。

這就是這個圖例里,針對于我們最初提出的問題可以演示的第一種情況,也是最簡單的一種情況。
當然了,這也是因為圖例中,首頁底部導航對應的三個fragment都隸屬于同一級,即主Activity當中。


<h1>getFragmentManager還是getChildFragmentManager()?</h1>

現在階段性總結一下,本文圖例中,首頁導航的三個fragment都屬于同一個FragmentMnanger管理。所以根據之前的源碼分析我們就可以得知:

在我們通過hide/show來切換兩個碎片顯示時,相對應的,它們的onHiddenChaned方法就會被回調,所以這個時候監聽它們的隱藏/顯示是很容易的。

那我們更進一步,比較特殊的是圖例中“第一頁”的界面,由圖可以看到其中有一個側滑菜單,菜單中的三個選項卡對又應另外三個fragment。

OK,那么現在思考一下!這三個fragment還和我們之前說到的首頁導航對應的三個fragment位于同一級別嗎?我們來分析一下。

首先,我們已經說到在代碼中動態的控制fragment,都借助于FragmentManager。而對應“第一頁”的FirstFragment本身也是一個fragment。

所以在這個時候,與之前我們在主Activity通過get(Support)FragmentManager有一點不同的是,在Fragment當中我們多了一個選擇:

我們發現有趣的是多了一個叫做getChildFragmentManager的方法,它們之間到底區別在哪呢?在主Activity和FirstFragment分別添加下如下日志:

// Activity
Log.d(TAG, getSupportFragmentManager()/*或者getFragmentManager()*/+"");
// FirstFragment
Log.d(TAG,getFragmentManager()+"\n"+getChildFragmentManager());

然后運行程序發現如下的日志打印:

由此我們可以發現,在FirstFragment中獲取的FragmentManager和之前在MainActivity中的是同一對象,歸屬于一個叫做HostCallBacks的東西管理。

與之不同的是:通過getChildFragmentManager獲取到的FragmentManager則是另一個不同的對象,而另一個不同在于它則屬于FirstFragment自身。

(P.S:關于HostCallBack這個東西,有興趣的朋友可以自己研究源碼或者關于Fragment源碼分析的文章。簡單的來說,它是屬于FragmentActivity的內部類,getSupportFragmentManager實際就是通過控制HostCallback返回FragmentManagerImpl對象)

好的,那么現在由此其實不難想象,既然Fragment會多出這么一個特定的方法,肯定是有其存在的意義的。

現在假定我們依然使用FragmentMananger在FirstFragment中管理側滑菜單的子碎片,那么首先可能會出現如下所示的問題:

這里可以看到的一個問題就是:當我們在底部導航由第一頁轉至第三頁后,第一頁的fragment中間的內容仍然沒有消失。

其實原因不難分析,因為前面說到如果此時使用getFragmentManager,意味著此時獲取到的FM對象其實和MainActivity中使用的FM是同一個對象。

也就是說,如此進行添加,側滑菜單對應的三個Fragment其實仍然是被add進了與FirstFragment隸屬的相同的FragmentManager的集合內。

那么,假定我們把側滑菜單對應的第一個fragment對象命名為Child1Fragment,這其實也就意味著:

我們視覺上看上去屬于第一頁(即FirstFragment)的內容其實本來真正應該是屬于Child1Fragment的內容。
但由于我們通過getFragmentManager進行add操作,我們通過如下代碼完成前面說到由第一頁跳轉至第三頁的操作則會導致:

ft.hide(first)
  .show(third)
  .commit;

雖然我們按照邏輯hide了FirstFragment對象,但關鍵在于:因為它們都屬于同一個FM對象,所以其實Child1Fragment仍然沒有被hide。

由此其實我們不難想象:如果通過FragmentManager在Fragment中嵌套Fragment,將由于邏輯的嚴重混亂,而造成難以管理

那么,與之對應的,如果選擇使用getChildFragmentManager的好處有哪些呢?我們可以簡單的概括一下:

首先,最重要的一點:通過ChildFragmentManager進行管理的子Fragment對象,與其父Fragment對象的生命周期是息息相關的

舉個例子,假設我們用SecondFragment對象來replace掉FirstFragment對象,這時候有了前面的基礎,我們都知道:

FristFragment將走入onPause開始的這段生命周期。而使用ChildFragmentManager的好處在于,其內部的子Fragment也會受相同的生命周期管理。

顯然,我們可以預見由此帶來的最大的好處就是:此時各個Fragment之間的邏輯清晰,層級分布明確,將大大利于我們對其進行管理

另一個非常實用的好處就在于:在這種管理模式下,子Fragment能夠很容易的實現與父Fragment之間進行通信。通過一個例子能更形象的理解。

依舊是本文最初的圖例,我在FirstFragment中放置了一個ToolBar,那么如果我切換了選項卡,想要在子Fragment的動態的操作Toolbar,就能這么做:

    public void doSomething(){
        // do what you want to do
    }

是的,首先在FirstFragment中我們提供這一樣一個回調方法,然后在子Fragment中我們就可以通過如下方式與其發生互動:

        FirstFragment parent = (FirstFragment) getParentFragment();
        parent.doSomething();

這都是一些非常實用的小技巧,更多的延伸實用,我們可以自己在實際中拓展。總之:如果是剛開始接觸Fragment,一定記住:

如果是在Fragment中添加Fragment時,請一定記住選擇通過getChildFragmentManager來對碎片進行管理

那么,現在我們言歸正傳。通過ChildFragmentManager是不是就能解決我們說到的監聽fragment隱藏/顯示了呢?

其實不難推測出答案。我們再次回到那個情景,在第三頁清楚緩存后,回到第一頁,那么很明顯符合我們邏輯的實現就是:

ft.hide(third)
  .show(first)
  .commit;

現在我相信我們都很清楚了,這時候通過onHiddenChanged肯定是能夠監聽到FirstFragment的,但對于嵌套在其內的Child1Fragment就不行了。

但是因為之前的基礎,這個問題顯然已不難解決。我們可以在FirstFragment中定義一個index來記錄此時的側滑菜單的子Fragment索引,隨后:

    // FirstFragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
        getChildFragmentManager().getFragments().get(currentIndex).onHiddenChanged(hidden);
    }

怎么樣,是不是很簡單呢?這就是我們本文中提到的問題的第二種場景延伸。所以說細節真的很能夠幫助我們更加靈活的掌握一件事物。


<h1>與ViewPager的配合</h1>

現在我們進一步深入,正如本文圖例所示,Child1Fragment中有一個ViewPager,ViewPager中有放置了兩個Fragment。

再次衍生我們之前的場景描述:現在在“第三頁”清除緩存過后,再次回到第一頁,此時第一頁顯示的正好是ViewPager中的碎片。

那么,這個時候我們應該如何監聽對應的這個位于ViewPager中的Fragment對象呢?相信有了之前的基礎,我們很容易類推出來。

既然上一節中我們已經將顯示狀態的改變由FirstFragment傳遞到了Child1Fragment,那么現在只需要繼續向ViewPager進行傳遞就行了。

    // Child1Fragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
       getChildFragmentManager().getFragments().get(mViewPager.getCurrentItem()).onHiddenChanged(hidden);
    }

這便是我們本文描述的問題的第三種場景延伸。但是關于fragment配合viewpager使用時,依然還有很多值得留意的小技巧和細節。

<h2>ViewPager的緩存機制</h2>

通過之前的分析與總結,我們已經清楚通過FragmentManager管理碎片時,Fragment的生命周期變化情況。

那么,當Fragment配合ViewPager時,Fragment的生命周期又是什么情況呢?我們還是可以自己驗證一下。

為了能得到更準確的結論,可以把Child1Fragment中ViewPager放置的Fragment數量添加到4個,再通過切換來查看各個碎片的生命周期。

為了節省篇幅,我們選擇查看兩個最具有代表性的生命周期變化的片段截圖,首先是ViewPager的初始顯示時的片段截圖:

可以看到雖然ViewPager初始時,只需要顯示第一個Fragment,但是第二個Fragment對象仍然經過了初始化的生命周期。接著:

假設我們進一步的操作是直接將ViewPager由第一個Fragment滑動至第三個,然后我們再來瞧一瞧對應的生命周期變化:

由此我們可以發現,這個時候不僅切換到的第三個Fragment進行了初始化,與它相鄰的第四個碎片同樣也進行了初始化。與此同時,可以發現:
在這之前顯示的第一個Fragment則經過了onPause到onDestoryView的生命周期變化,也就是說這時第一個Fragment的視圖會被銷毀。

這其實就是ViewPager自身的一個緩存機制,默認情況下它會幫我們緩存一個Fragment相鄰的兩個Fragment對象。簡單來說,就像上面表現的:

當第一個Fragment需要顯示時,其相鄰的第二個對象也會進行初始化。第三個Fragment需要顯示時,左邊第二個對象已經完成了初始化,于是右邊的第四個則會進行初始化。

我們不難推測出設計者如此設計的初衷:顯然這是為了用戶對ViewPager有更好的體驗,設想一下:

  • 當用戶進入到ViewPager的第一個視圖,這時相鄰的第二個視圖也已經進行了初始化。那么當用戶切換到第二頁,則可以直接進行瀏覽了。
  • 當用戶切換到第三頁的時候,之所以選擇銷毀掉第一頁的視圖,則是為了減少嵌入的Fragment數量,減少滑動時出現卡頓的可能性。

<h2>setOffscreenPageLimit</h2>

了解了ViewPager的緩存機制,則有一個比較實用的東西叫做setOffscreenPageLimit,它的作用就是來設置這個緩存的上限。

這個上限的默認值為1,而當該值為1時,其效果就和我們上一節描述的一樣。我們可以自己設置該值來改變這個緩存的數量。

但與此同時,需要注意的另一個細節是,要避免做出類似如下代碼所示的這種想當然的操作:

mViewPager.setOffscreenPageLimit(0);

這行代碼是無法完成你本來想要實現的目的的,究其原因,可以在源碼中找到答案:

從代碼中不難看到,當我們傳入的limit參數小于DEFAULT_OFFSCREEN_PAGES時,就將直接被設置為等同于這個默認值。

那么DEFAULT_OFFSCREEN_PAGES的值究竟是多少呢?其實從前面的分析就能得出結論,其值為1:

<h2>setUserVisibleHint</h2>

我們也許已經留意到,在前面的生命周期變化中,有一個叫做setUserVisibleHint的東西反復出現了不少次。其實有了之前的基礎,就容易理解了。

之前通過FragmentManager控制碎片的隱藏和顯示,回調的是onHiddenChanged方法。而在ViewPager則沒有通過FM來進行控制。
所以不難推測,這時Fragment對象的顯示和隱藏,回調多半就不是onHiddenChanged了。事實正是如此,此時的回調則是setUserVisibleHint。

所以說,如果想要在這種情況監聽Fragment對象的隱藏/顯示,那么監聽這個方法就可以了。這也理解為我們本文提出的問題的第四種場景延伸。

最后,這里注意一下這種情景與我們之前描述的第三種場景的區別。之前說到的對于ViewPager中的碎片的顯示/隱藏狀態監聽的解決方案,是從針對從其它Fragment切換到ViewPager中的某個Fragment顯示的情景。而監聽setUserVisibleHint則是針對于都是位于ViewPager內的Fragment對象相互之前的切換顯示的情況。


<h2>ViewPager的懶加載</h2>
其實寫到這里,對于我能想到的關于本文最初說到的那個朋友提出的問題 常見的情景延伸都已經總結到了,本想結束。

但是前面說到ViewPager的緩存機制時,我們提到ViewPager會根據設置的緩存數量上限來控制相應數量的Fragment對象提前初始化。

這就可能涉及到另一個比較實用的小技巧:配合ViewPager時,Fragment的懶加載。雖然網上該類資料很多,但還是可以簡單總結一下。

所謂的懶加載其實很好理解,我們說了正常情況下,ViewPager會對某指定數量的Fragment進行預初始化。

而通常在Fragment初始化的生命周期里:我們都會做一些與該Fragment相關的數據的加載工作等等。那么:
在一些時候,比如某個Fragment在初始化時需要加載的數據量較大;或者說因為數據來源于網絡等原因,
此時等待該Fragment完成初始化,就會從一定程度上影響到應用的效率,這個時候就產生了所謂的“懶加載”的需求。

所以其實懶加載的本質非常簡單:那就是不要在ViewPager預初始化的時候去加載數據,而是當該Fragment真正顯示時才進行加載。

而因為我們之前已經知道了setUserVisibleHint這個東西,所以其實解決方案就不難給出了。這里可以給出一個簡單的模板,僅供參考:

public abstract class LazyLoadFragment extends Fragment{

    protected boolean isPrepared;
    protected boolean isLoadedOnce;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutId(), container, false);

        isPrepared = true;
        // 數據加載
        loadData();

        return view;
    }

    protected abstract int getLayoutId() ;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        loadData();
    }

    protected void loadData() {
        if(!isPrepared || !getUserVisibleHint() || isLoadedOnce)
            return;
        // 懶加載
        lazyLoad();
        isLoadedOnce = true;
    }

    protected abstract void lazyLoad();

    @Override
    public void onDetach() {
        super.onDetach();
        isPrepared = isLoadedOnce = false;
    }

}

上面的代碼應該不難理解,如果需要實現懶加載,則可以讓Fragment繼承該類,然后再覆寫用于加載數據的lazyLoad方法就可以了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容